Skip to content

Commit

Permalink
Merge pull request #1453 from merico-dev/1452-drag-to-re-arrange-arra…
Browse files Browse the repository at this point in the history
…y-items

1452 drag to re arrange array items
  • Loading branch information
GerilLeto authored Jul 1, 2024
2 parents 72c1ed1 + 84062c0 commit 27d3f01
Show file tree
Hide file tree
Showing 11 changed files with 384 additions and 75 deletions.
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devtable/api",
"version": "13.19.0",
"version": "13.20.0",
"description": "",
"main": "index.js",
"scripts": {
Expand Down
5 changes: 4 additions & 1 deletion dashboard/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devtable/dashboard",
"version": "13.19.0",
"version": "13.20.0",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
Expand Down Expand Up @@ -34,6 +34,9 @@
"coverage": "vitest --coverage"
},
"dependencies": {
"@dnd-kit/helpers": "^0.0.3",
"@dnd-kit/react": "^0.0.4",
"@dnd-kit/sortable": "^8.0.0",
"@json2csv/whatwg": "7.0.3",
"@monaco-editor/react": "4.4.6",
"@types/chroma-js": "^2.1.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,73 @@
import { Badge, CloseButton, ColorInput, Stack, Table } from '@mantine/core';
import { move } from '@dnd-kit/helpers';
import { DragDropProvider } from '@dnd-kit/react';
import { useSortable } from '@dnd-kit/react/sortable';
import { ActionIcon, Badge, Center, CloseButton, ColorInput, Flex, Stack } from '@mantine/core';
import { IconGripVertical } from '@tabler/icons-react';
import { useBoolean } from 'ahooks';
import { Ref, forwardRef, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { v4 as uuidv4 } from 'uuid';
import { PreviewGradientAndApplyPalette } from './preview-gradient-and-apply-palette';

type ColorFieldItem = {
id: string;
value: string;
};

type ColorRowProps = {
color: ColorFieldItem;
handleChange: (v: string) => void;
handleRemove: () => void;
index: number;
};
const ColorRow = ({ color, index, handleChange, handleRemove }: ColorRowProps) => {
const [hovering, { setTrue, setFalse }] = useBoolean(false);
const { ref, handleRef } = useSortable({
id: color.id,
index,
});

return (
<Flex
ref={ref}
gap="sm"
justify="flex-start"
align="center"
direction="row"
wrap="nowrap"
onMouseEnter={setTrue}
onMouseLeave={setFalse}
>
<Center style={{ minWidth: '30px', maxWidth: '30px', flex: 0 }}>
{hovering ? (
<ActionIcon size="xs" ref={handleRef}>
<IconGripVertical />
</ActionIcon>
) : (
<Badge size="sm">{index + 1}</Badge>
)}
</Center>
<div style={{ flex: 1 }}>
<ColorInput
styles={{
root: {
flexGrow: 1,
},
}}
withinPortal
dropdownZIndex={340}
size="xs"
value={color.value}
onChange={handleChange}
/>
</div>
<div style={{ minWidth: '40px', maxWidth: '40px', flex: 0 }}>
<CloseButton onClick={handleRemove} />
</div>
</Flex>
);
};

type Props = {
value: string[];
onChange: (v: string[]) => void;
Expand Down Expand Up @@ -42,53 +106,41 @@ export const GradientEditor = forwardRef(({ value, onChange }: Props, ref: Ref<H
setNewColor('');
}
};
const onDragEnd = (event: any) => {
const { source, target } = event.operation;
const newColors = move(colors, source, target);
onChange(newColors.map((c) => c.value));
};

return (
<Stack ref={ref}>
<PreviewGradientAndApplyPalette colors={value} applyPalette={replace} />
<Table withBorder={false} withColumnBorders={false} sx={{ td: { borderTop: 'none !important' } }}>
<tbody>
{colors.map((c, index) => (
<tr key={c.id}>
<td style={{ width: '60px' }}>
<Badge>{index}</Badge>
</td>
<td>
<ColorInput
styles={{
root: {
flexGrow: 1,
},
}}
withinPortal
dropdownZIndex={340}
size="xs"
value={c.value}
onChange={getChangeHandler(index)}
/>
</td>
<td style={{ width: '40px' }}>
<CloseButton onClick={() => remove(index)} />
</td>
</tr>
))}
<tr>
<td style={{ width: '60px' }} />
<td>
<ColorInput
withinPortal
dropdownZIndex={340}
placeholder={t('chart.color.click_to_add_a_color')}
value={newColor}
onChange={setNewColor}
onBlur={addNewColor}
size="xs"
/>
</td>
<td style={{ width: '40px' }} />
</tr>
</tbody>
</Table>
<DragDropProvider onDragEnd={onDragEnd}>
{colors.map((c, index) => (
<ColorRow
key={c.id}
color={c}
handleChange={getChangeHandler(index)}
handleRemove={() => remove(index)}
index={index}
/>
))}
</DragDropProvider>
<Flex gap="sm" justify="flex-start" align="center" direction="row" wrap="nowrap">
<div style={{ minWidth: '30px', maxWidth: '30px', flex: 0 }} />
<div style={{ flex: 1 }}>
<ColorInput
withinPortal
dropdownZIndex={340}
placeholder={t('chart.color.click_to_add_a_color')}
value={newColor}
onChange={setNewColor}
onBlur={addNewColor}
size="xs"
/>
</div>
<div style={{ minWidth: '40px', maxWidth: '40px', flex: 0 }} />
</Flex>
</Stack>
);
});
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { Button, Center, Divider, Stack, Tabs, Tooltip } from '@mantine/core';
import { IconPlus, IconTrash } from '@tabler/icons-react';
import { ReactNode } from 'react';
import { Button, Divider, Stack, Tabs } from '@mantine/core';
import { IconTrash } from '@tabler/icons-react';
import _ from 'lodash';
import { ReactNode, useEffect, useState } from 'react';
import { ArrayPath, Control, FieldValues, Path, UseFormWatch, useFieldArray } from 'react-hook-form';
import { TabList } from './tab-list';
import { ControlledField, FieldArrayTabsChildren } from './types';

const TabsStyles = {
tab: {
paddingTop: '0px',
paddingBottom: '0px',
paddingTop: '4px',
paddingBottom: '4px',
},
panel: {
padding: '0px',
},
};

type FieldArrayTabsChildren<FieldItem> = ({ field, index }: { field: FieldItem; index: number }) => ReactNode;
export type FieldArrayButtonStateFunc<FieldItem> = ({
field,
index,
Expand All @@ -35,7 +37,7 @@ type Props<T extends FieldValues, FieldItem> = {
renderTabName: (field: FieldItem, index: number) => ReactNode;
deleteDisalbed?: FieldArrayButtonStateFunc<FieldItem>;
};
// TODO: first selected tab

export const FieldArrayTabs = <T extends FieldValues, FieldItem>({
control,
watch,
Expand All @@ -47,39 +49,54 @@ export const FieldArrayTabs = <T extends FieldValues, FieldItem>({
renderTabName,
deleteDisalbed,
}: Props<T, FieldItem>) => {
const { fields, append, remove } = useFieldArray({
const fieldArray = useFieldArray({
control,
name: name as ArrayPath<T>,
});
const { fields, append, remove } = fieldArray;

const watchFieldArray = watch(name);
const controlledFields = fields.map((field, index) => {
return {
...field,
...watchFieldArray[index],
};
} as ControlledField<T>;
});

const defaultTab = _.last(controlledFields)?.id ?? null;
const [tab, setTab] = useState<string | null>(defaultTab);
const handleTabChange = (tab: string | null) => {
if (tab === 'add') {
return;
}
setTab(tab);
};
useEffect(() => {
setTab((t) => {
if (defaultTab === t) {
return t;
}
return defaultTab;
});
}, [defaultTab]);

const add = () => {
append(getItem());
const item = getItem();
fieldArray.append(item);
setTab(item.id);
};

return (
<Tabs defaultValue="0" styles={TabsStyles}>
<Tabs.List>
{controlledFields.map((field, index) => (
<Tabs.Tab key={field.id} value={index.toString()}>
{renderTabName(field, index)}
</Tabs.Tab>
))}
<Tabs.Tab onClick={add} value="add">
<Tooltip label={addButtonText}>
<Center>
<IconPlus size={18} color="#228be6" />
</Center>
</Tooltip>
</Tabs.Tab>
</Tabs.List>
<Tabs value={tab} onTabChange={handleTabChange} styles={TabsStyles}>
<TabList<T, FieldItem>
fieldArray={fieldArray}
add={add}
addButtonText={addButtonText}
renderTabName={renderTabName}
controlledFields={controlledFields}
/>
{controlledFields.map((field, index) => (
<Tabs.Panel key={field.id} value={index.toString()}>
<Tabs.Panel key={field.id} value={field.id}>
<Stack>
{children({ field, index })}
<Divider mb={-10} mt={10} variant="dashed" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { DragDropProvider } from '@dnd-kit/react';
import { useSortable } from '@dnd-kit/react/sortable';
import { Center, Tabs, Tooltip } from '@mantine/core';
import { IconGripHorizontal, IconPlus } from '@tabler/icons-react';
import { useBoolean } from 'ahooks';
import { ReactNode } from 'react';
import { ArrayPath, FieldValues, UseFieldArrayReturn } from 'react-hook-form';
import { ControlledField } from './types';

type DraggableTabProps = {
index: number;
value: string;
children: ReactNode;
};

const DraggableTab = ({ value, index, children }: DraggableTabProps) => {
const [hovering, { setTrue, setFalse }] = useBoolean(false);
const { ref, handleRef } = useSortable({
id: value,
index,
});

return (
<Tabs.Tab
ref={ref}
value={value}
icon={<IconGripHorizontal size={14} color={hovering ? 'rgb(34, 139, 230)' : 'transparent'} />}
onMouseEnter={setTrue}
onMouseLeave={setFalse}
>
{children}
</Tabs.Tab>
);
};

type Props<T extends FieldValues, FieldItem> = {
fieldArray: UseFieldArrayReturn<T, ArrayPath<T>, 'id'>;
addButtonText: string;
add: () => void;
renderTabName: (field: FieldItem, index: number) => ReactNode;
controlledFields: ControlledField<T>[];
};

export const TabList = <T extends FieldValues, FieldItem>({
fieldArray,
add,
addButtonText,
renderTabName,
controlledFields,
}: Props<T, FieldItem>) => {
const onDragEnd = (event: any) => {
const { source, target } = event.operation;
const fromIndex = controlledFields.findIndex((f) => f.id === source.id);
const toIndex = target.index;
fieldArray.move(fromIndex, toIndex);
};

return (
<Tabs.List>
<DragDropProvider onDragEnd={onDragEnd}>
{controlledFields.map((field, index) => (
<DraggableTab key={field.id} value={field.id} index={index}>
{renderTabName(field, index)}
</DraggableTab>
))}
</DragDropProvider>

<Tabs.Tab onClick={add} value="add">
<Tooltip label={addButtonText}>
<Center>
<IconPlus size={18} color="#228be6" />
</Center>
</Tooltip>
</Tabs.Tab>
</Tabs.List>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ReactNode } from 'react';
import { ArrayPath, FieldArrayWithId, FieldValues, Path, PathValue } from 'react-hook-form';

export type ControlledField<T extends FieldValues> = FieldArrayWithId<T, ArrayPath<T>, 'id'> & PathValue<T, Path<T>>;
export type FieldArrayTabsChildren<FieldItem> = ({ field, index }: { field: FieldItem; index: number }) => ReactNode;
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ type PartialSeriesConfType = {
};

function getNumbersFromData(seriesData: SeriesDataIn): SeriesDataOut {
if (!seriesData) {
return [];
}
try {
if (Array.isArray(seriesData[0]) && seriesData[0]?.length >= 2) {
const s = seriesData as [string | number, number][];
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devtable/root",
"version": "13.19.0",
"version": "13.20.0",
"private": true,
"workspaces": [
"api",
Expand Down
Loading

0 comments on commit 27d3f01

Please sign in to comment.