Skip to content

Commit c4bd05e

Browse files
authored
Merge pull request #537 from Lemoncode/feature/#533-Copy-table
Feature/#533 copy table
2 parents fde2640 + 6d0d31f commit c4bd05e

16 files changed

+220
-1
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const CopyIcon = () => {
2+
return (
3+
<svg
4+
xmlns="http://www.w3.org/2000/svg"
5+
width="1.2em"
6+
height="1.2em"
7+
viewBox="0 0 256 256"
8+
>
9+
<path
10+
fill="currentColor"
11+
d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z"
12+
/>
13+
</svg>
14+
);
15+
};

src/common/components/icons/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ export * from './add-folder.component';
2222
export * from './down-icon';
2323
export * from './up-icon.component';
2424
export * from './remove-icon.component';
25+
export * from './copy-icon.component';
26+
export * from './paste-icon.component';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const PasteIcon = () => {
2+
return (
3+
<svg
4+
xmlns="http://www.w3.org/2000/svg"
5+
width="1.2em"
6+
height="1.2em"
7+
viewBox="0 0 256 256"
8+
>
9+
<path
10+
fill="currentColor"
11+
d="M200 32h-36.26a47.92 47.92 0 0 0-71.48 0H56a16 16 0 0 0-16 16v168a16 16 0 0 0 16 16h144a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16m-72 0a32 32 0 0 1 32 32H96a32 32 0 0 1 32-32m72 184H56V48h26.75A47.9 47.9 0 0 0 80 64v8a8 8 0 0 0 8 8h80a8 8 0 0 0 8-8v-8a47.9 47.9 0 0 0-2.75-16H200Z"
12+
/>
13+
</svg>
14+
);
15+
};

src/core/providers/canvas-schema/canvas-schema-vlatest.model.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,8 @@ export interface CanvasSchemaContextVm {
7878
updateFullRelation: (relation: RelationVm) => void;
7979
deleteSelectedItem: (selectedElementId: GUID) => void;
8080
switchIsPristine: (isPristine: boolean) => void;
81+
duplicateSelectedTable: () => void;
82+
copySelectedTable: () => void;
83+
pasteTable: () => void;
84+
hasClipboardContent: boolean;
8185
}

src/core/providers/canvas-schema/canvas-schema.provider.tsx

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import { produce } from 'immer';
33
import { CanvasSchemaContext } from './canvas-schema.context';
44
import {
@@ -157,6 +157,75 @@ export const CanvasSchemaProvider: React.FC<Props> = props => {
157157
}));
158158
};
159159

160+
const duplicateSelectedTable = () => {
161+
setSchema(prevSchema => {
162+
if (!prevSchema.selectedElementId) return prevSchema;
163+
164+
const selectedTable = prevSchema.tables.find(
165+
table => table.id === prevSchema.selectedElementId
166+
);
167+
168+
if (!selectedTable) return prevSchema;
169+
170+
// Create duplicate with new IDs
171+
const duplicateTable: TableVm = {
172+
...selectedTable,
173+
id: crypto.randomUUID(), // Generate new ID
174+
x: selectedTable.x + 50, // Offset position
175+
y: selectedTable.y + 50,
176+
fields: selectedTable.fields.map(field => ({
177+
...field,
178+
id: crypto.randomUUID(), // New IDs for fields
179+
})),
180+
};
181+
182+
return {
183+
...prevSchema,
184+
tables: [...prevSchema.tables, duplicateTable],
185+
isPristine: false,
186+
};
187+
});
188+
};
189+
190+
const [clipboardTable, setClipboardTable] = useState<TableVm | null>(null);
191+
const [pasteOffset, setPasteOffset] = useState({ x: 0, y: 0 });
192+
193+
const copySelectedTable = () => {
194+
const selectedTable = canvasSchema.tables.find(
195+
table => table.id === canvasSchema.selectedElementId
196+
);
197+
if (selectedTable) {
198+
setClipboardTable(selectedTable);
199+
setPasteOffset({ x: 0, y: 0 }); // Reset offset on copy
200+
}
201+
};
202+
203+
const pasteTable = () => {
204+
if (clipboardTable) {
205+
// Increment offset
206+
const newOffset = { x: pasteOffset.x + 50, y: pasteOffset.y + 50 };
207+
208+
const newTable: TableVm = {
209+
...clipboardTable,
210+
id: crypto.randomUUID(),
211+
x: clipboardTable.x + newOffset.x,
212+
y: clipboardTable.y + newOffset.y,
213+
fields: clipboardTable.fields.map(field => ({
214+
...field,
215+
id: crypto.randomUUID(),
216+
})),
217+
};
218+
219+
setSchema(prev => ({
220+
...prev,
221+
tables: [...prev.tables, newTable],
222+
isPristine: false,
223+
}));
224+
225+
setPasteOffset(newOffset);
226+
}
227+
};
228+
160229
return (
161230
<CanvasSchemaContext.Provider
162231
value={{
@@ -177,6 +246,10 @@ export const CanvasSchemaProvider: React.FC<Props> = props => {
177246
updateFullRelation,
178247
deleteSelectedItem,
179248
switchIsPristine: switchIsPristine,
249+
duplicateSelectedTable,
250+
copySelectedTable,
251+
pasteTable,
252+
hasClipboardContent: Boolean(clipboardTable),
180253
}}
181254
>
182255
{children}

src/pods/canvas/canvas.pod.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export const CanvasPod: React.FC = () => {
4242
doRedo,
4343
deleteSelectedItem,
4444
loadSchema,
45+
duplicateSelectedTable,
46+
copySelectedTable,
47+
pasteTable,
4548
} = useCanvasSchemaContext();
4649
const {
4750
canvasViewSettings,
@@ -221,6 +224,24 @@ export const CanvasPod: React.FC = () => {
221224
deleteSelectedItem(canvasSchema.selectedElementId);
222225
}
223226
}
227+
228+
// Add Cmd/Ctrl+D for duplicate
229+
if ((e.metaKey || e.ctrlKey) && e.key === 'd') {
230+
e.preventDefault(); // Prevent browser default
231+
duplicateSelectedTable();
232+
}
233+
234+
// Add Cmd/Ctrl+C for copy
235+
if ((e.metaKey || e.ctrlKey) && e.key === 'c') {
236+
e.preventDefault(); // Prevent browser default
237+
copySelectedTable();
238+
}
239+
240+
// Add Cmd/Ctrl+V for paste
241+
if ((e.metaKey || e.ctrlKey) && e.key === 'v') {
242+
e.preventDefault(); // Prevent browser default
243+
pasteTable();
244+
}
224245
};
225246

226247
modalDialog.isOpen
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useCanvasSchemaContext } from '@/core/providers/canvas-schema';
2+
import { CopyIcon } from '@/common/components/icons';
3+
import { ToolbarButton } from '@/pods/toolbar/components/toolbar-button';
4+
import classes from '@/pods/toolbar/toolbar.pod.module.css';
5+
import { SHORTCUTS } from '../../shortcut/shortcut.const';
6+
7+
export const CopyButton = () => {
8+
const { canvasSchema, copySelectedTable } = useCanvasSchemaContext();
9+
10+
return (
11+
<ToolbarButton
12+
icon={<CopyIcon />}
13+
label="Copy"
14+
onClick={copySelectedTable}
15+
className={`${classes.button} hide-mobile`}
16+
disabled={!canvasSchema.selectedElementId}
17+
shortcutOptions={SHORTCUTS.copy}
18+
/>
19+
);
20+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './copy-button.component';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useCanvasSchemaContext } from '@/core/providers/canvas-schema';
2+
import { CopyIcon } from '@/common/components/icons';
3+
import { ToolbarButton } from '@/pods/toolbar/components/toolbar-button';
4+
import classes from '@/pods/toolbar/toolbar.pod.module.css';
5+
import { SHORTCUTS } from '../../shortcut/shortcut.const';
6+
7+
export const DuplicateButton = () => {
8+
const { canvasSchema, duplicateSelectedTable } = useCanvasSchemaContext();
9+
10+
return (
11+
<ToolbarButton
12+
icon={<CopyIcon />}
13+
label="Duplicate Table"
14+
onClick={duplicateSelectedTable}
15+
className={`${classes.button} hide-mobile`}
16+
disabled={!canvasSchema.selectedElementId}
17+
shortcutOptions={SHORTCUTS.duplicate}
18+
/>
19+
);
20+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './duplicate-button.component';

src/pods/toolbar/components/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ export * from './redo-button';
1212
export * from './delete-button';
1313
export * from './add-collection';
1414
export * from './about-button';
15+
export * from './duplicate-button';
16+
export * from './copy-button';
17+
export * from './paste-button';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './paste-button.component';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useCanvasSchemaContext } from '@/core/providers/canvas-schema';
2+
import { PasteIcon } from '@/common/components/icons';
3+
import { ToolbarButton } from '@/pods/toolbar/components/toolbar-button';
4+
import classes from '@/pods/toolbar/toolbar.pod.module.css';
5+
import { SHORTCUTS } from '../../shortcut/shortcut.const';
6+
7+
export const PasteButton = () => {
8+
const { pasteTable, hasClipboardContent } = useCanvasSchemaContext();
9+
10+
return (
11+
<ToolbarButton
12+
icon={<PasteIcon />}
13+
label="Paste"
14+
onClick={pasteTable}
15+
className={`${classes.button} hide-mobile`}
16+
disabled={!hasClipboardContent}
17+
shortcutOptions={SHORTCUTS.Paste}
18+
/>
19+
);
20+
};

src/pods/toolbar/shortcut/shortcut.const.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,22 @@ export const SHORTCUTS: Shortcut = {
7777
targetKey: ['-', '-'],
7878
targetKeyLabel: '"-"',
7979
},
80+
duplicate: {
81+
description: 'Duplicate',
82+
id: 'duplicate-button-shortcut',
83+
targetKey: ['d'],
84+
targetKeyLabel: 'D',
85+
},
86+
copy: {
87+
description: 'Copy',
88+
id: 'copy-button-shortcut',
89+
targetKey: ['c'],
90+
targetKeyLabel: 'C',
91+
},
92+
paste: {
93+
description: 'Paste',
94+
id: 'paste-button-shortcut',
95+
targetKey: ['v'],
96+
targetKeyLabel: 'V',
97+
},
8098
};

src/pods/toolbar/toolbar.pod.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
DeleteButton,
1616
AboutButton,
1717
CanvasSettingButton,
18+
CopyButton,
19+
PasteButton,
1820
} from './components';
1921
import classes from './toolbar.pod.module.css';
2022

@@ -28,6 +30,8 @@ export const ToolbarPod: React.FC = () => {
2830
<ZoomOutButton />
2931
<AddCollection />
3032
<RelationButton />
33+
<CopyButton />
34+
<PasteButton />
3135
<UndoButton />
3236
<RedoButton />
3337
<ExportButton />

0 commit comments

Comments
 (0)