Skip to content

Commit 21dfae4

Browse files
authored
Merge pull request #538 from Lemoncode/vnext
copy paste functionallity
2 parents afcdb7e + c4bd05e commit 21dfae4

File tree

20 files changed

+277
-7
lines changed

20 files changed

+277
-7
lines changed

.github/workflows/ci.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,27 @@ jobs:
2222
run: npm run tsc-check
2323
- name: Tests front
2424
run: npm test
25+
26+
e2e-tests:
27+
runs-on: ubuntu-latest
28+
steps:
29+
- name: Checkout repository
30+
uses: actions/checkout@v2
31+
32+
- name: Use Node.js 18.13.0
33+
uses: actions/setup-node@v2
34+
with:
35+
node-version: '18.13.0'
36+
cache: 'npm'
37+
38+
- name: Install dependencies
39+
run: npm ci
40+
41+
- name: Build
42+
run: npm run build
43+
44+
- name: Check TypeScript Types
45+
run: npm run tsc-check
46+
47+
- name: Run E2E tests
48+
run: npm run ci:e2e

e2e/add-new-collection.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test('opens MongoDB Designer, adds collection, and checks "New Collection" visibility', async ({
4+
page,
5+
}) => {
6+
await page.goto('');
7+
8+
await page.getByRole('link', { name: 'Launch MongoDB Designer' }).click();
9+
await expect(page).toHaveURL('http://localhost:5173/editor.html');
10+
11+
const newButton = page.getByRole('button', { name: 'New' });
12+
await expect(newButton).toBeVisible();
13+
await newButton.click();
14+
15+
const addCollectionButton = page
16+
.getByRole('button', { name: 'Add Collection' })
17+
.first();
18+
await expect(addCollectionButton).toBeVisible();
19+
await addCollectionButton.click();
20+
21+
const applyButton = page.getByRole('button', { name: 'Apply' });
22+
await expect(applyButton).toBeVisible();
23+
await expect(applyButton).toBeEnabled();
24+
await applyButton.click();
25+
26+
const newCollectionText = page.locator('svg g text', {
27+
hasText: 'New Collection',
28+
});
29+
await expect(newCollectionText).toBeVisible();
30+
});

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.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"test": "vitest",
1515
"prepare": "husky || \"No need to install husky\"",
1616
"tsc-check": "tsc --noEmit",
17-
"e2e": "playwright test --ui"
17+
"e2e": "playwright test --ui",
18+
"ci:e2e": "playwright test"
1819
},
1920
"dependencies": {
2021
"@lemoncode/fonk": "^1.5.4",

playwright.config.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ export default defineConfig({
2626
use: { ...devices['Desktop Firefox'] },
2727
},
2828

29-
{
30-
name: 'webkit',
31-
use: { ...devices['Desktop Safari'] },
32-
},
33-
3429
/* Test against mobile viewports. */
3530
// {
3631
// name: 'Mobile Chrome',
@@ -54,6 +49,7 @@ export default defineConfig({
5449
/* Run your local dev server before starting the tests */
5550
webServer: {
5651
command: 'npm run dev',
52+
url: BASE_URL,
5753
reuseExistingServer: !process.env.CI,
5854
},
5955
});
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)