@vendure/dashboard 3.4.3-master-202509180227 → 3.4.3-master-202509190229
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -7
- package/src/app/common/duplicate-bulk-action.tsx +37 -23
- package/src/app/common/duplicate-entity-dialog.tsx +117 -0
- package/src/lib/components/data-input/rich-text-input.tsx +2 -115
- package/src/lib/components/shared/rich-text-editor/image-dialog.tsx +223 -0
- package/src/lib/components/shared/rich-text-editor/link-dialog.tsx +151 -0
- package/src/lib/components/shared/rich-text-editor/responsive-toolbar.tsx +439 -0
- package/src/lib/components/shared/rich-text-editor/rich-text-editor.tsx +338 -0
- package/src/lib/components/shared/rich-text-editor/table-delete-menu.tsx +104 -0
- package/src/lib/components/shared/rich-text-editor/table-edit-icons.tsx +225 -0
- package/src/lib/graphql/common-operations.ts +19 -0
- package/src/lib/graphql/fragments.ts +23 -13
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { FloatingMenu } from '@tiptap/extension-floating-menu';
|
|
2
|
+
import Image from '@tiptap/extension-image';
|
|
3
|
+
import { TableKit } from '@tiptap/extension-table';
|
|
4
|
+
import { TextStyle } from '@tiptap/extension-text-style';
|
|
5
|
+
import { EditorContent, useEditor } from '@tiptap/react';
|
|
6
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
7
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
8
|
+
import { ResponsiveToolbar } from './responsive-toolbar.js';
|
|
9
|
+
import { TableDeleteMenu } from './table-delete-menu.js';
|
|
10
|
+
import { TableEditIcons } from './table-edit-icons.js';
|
|
11
|
+
|
|
12
|
+
const extensions = [
|
|
13
|
+
TextStyle.configure(),
|
|
14
|
+
StarterKit.configure({
|
|
15
|
+
bulletList: {
|
|
16
|
+
keepMarks: true,
|
|
17
|
+
keepAttributes: false,
|
|
18
|
+
},
|
|
19
|
+
orderedList: {
|
|
20
|
+
keepMarks: true,
|
|
21
|
+
keepAttributes: false,
|
|
22
|
+
},
|
|
23
|
+
link: {
|
|
24
|
+
openOnClick: false,
|
|
25
|
+
HTMLAttributes: {
|
|
26
|
+
class: 'text-primary underline underline-offset-2 cursor-pointer hover:text-primary/80',
|
|
27
|
+
},
|
|
28
|
+
validate: href => /^https?:\/\//.test(href),
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
Image.configure({
|
|
32
|
+
inline: true,
|
|
33
|
+
allowBase64: true,
|
|
34
|
+
HTMLAttributes: {
|
|
35
|
+
class: 'rich-text-image',
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
TableKit.configure({
|
|
39
|
+
tableCell: {
|
|
40
|
+
HTMLAttributes: {
|
|
41
|
+
class: 'rich-text-table-cell',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
tableHeader: {
|
|
45
|
+
HTMLAttributes: {
|
|
46
|
+
class: 'rich-text-table-header',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
FloatingMenu.configure({
|
|
51
|
+
shouldShow: null, // Let individual floating menus control when they show
|
|
52
|
+
}),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export interface RichTextEditorProps {
|
|
56
|
+
value: string;
|
|
57
|
+
onChange: (value: string) => void;
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function RichTextEditor({ value, onChange, disabled = false }: Readonly<RichTextEditorProps>) {
|
|
62
|
+
const isInternalUpdate = useRef(false);
|
|
63
|
+
|
|
64
|
+
const editor = useEditor({
|
|
65
|
+
parseOptions: {
|
|
66
|
+
preserveWhitespace: 'full',
|
|
67
|
+
},
|
|
68
|
+
extensions: extensions,
|
|
69
|
+
content: value,
|
|
70
|
+
editable: !disabled,
|
|
71
|
+
onUpdate: ({ editor }) => {
|
|
72
|
+
if (!disabled) {
|
|
73
|
+
isInternalUpdate.current = true;
|
|
74
|
+
const newValue = editor.getHTML();
|
|
75
|
+
if (value !== newValue) {
|
|
76
|
+
onChange(newValue);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
editorProps: {
|
|
81
|
+
attributes: {
|
|
82
|
+
class: `rich-text-editor placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/10 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive field-sizing-content min-h-16 w-full bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm max-h-[500px] overflow-y-auto ${disabled ? 'cursor-not-allowed opacity-50' : ''}`,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
useLayoutEffect(() => {
|
|
88
|
+
if (editor && !isInternalUpdate.current) {
|
|
89
|
+
const currentContent = editor.getHTML();
|
|
90
|
+
if (currentContent !== value) {
|
|
91
|
+
const { from, to } = editor.state.selection;
|
|
92
|
+
editor.commands.setContent(value, { emitUpdate: false });
|
|
93
|
+
editor.commands.setTextSelection({ from, to });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
isInternalUpdate.current = false;
|
|
97
|
+
}, [value, editor]);
|
|
98
|
+
|
|
99
|
+
useLayoutEffect(() => {
|
|
100
|
+
if (editor) {
|
|
101
|
+
editor.setEditable(!disabled, false);
|
|
102
|
+
}
|
|
103
|
+
}, [disabled, editor]);
|
|
104
|
+
|
|
105
|
+
if (!editor) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="border rounded-md overflow-hidden">
|
|
111
|
+
<ResponsiveToolbar editor={editor} disabled={disabled} />
|
|
112
|
+
<EditorContent editor={editor} />
|
|
113
|
+
<TableEditIcons editor={editor} disabled={disabled} />
|
|
114
|
+
<TableDeleteMenu editor={editor} disabled={disabled} />
|
|
115
|
+
<style>{`
|
|
116
|
+
.rich-text-editor h1 {
|
|
117
|
+
font-size: 2em;
|
|
118
|
+
font-weight: 700;
|
|
119
|
+
margin-top: 0.67em;
|
|
120
|
+
margin-bottom: 0.67em;
|
|
121
|
+
line-height: 1.2;
|
|
122
|
+
}
|
|
123
|
+
.rich-text-editor h2 {
|
|
124
|
+
font-size: 1.5em;
|
|
125
|
+
font-weight: 600;
|
|
126
|
+
margin-top: 0.83em;
|
|
127
|
+
margin-bottom: 0.83em;
|
|
128
|
+
line-height: 1.3;
|
|
129
|
+
}
|
|
130
|
+
.rich-text-editor h3 {
|
|
131
|
+
font-size: 1.17em;
|
|
132
|
+
font-weight: 600;
|
|
133
|
+
margin-top: 1em;
|
|
134
|
+
margin-bottom: 1em;
|
|
135
|
+
line-height: 1.4;
|
|
136
|
+
}
|
|
137
|
+
.rich-text-editor h4 {
|
|
138
|
+
font-size: 1em;
|
|
139
|
+
font-weight: 600;
|
|
140
|
+
margin-top: 1.33em;
|
|
141
|
+
margin-bottom: 1.33em;
|
|
142
|
+
line-height: 1.4;
|
|
143
|
+
}
|
|
144
|
+
.rich-text-editor h5 {
|
|
145
|
+
font-size: 0.83em;
|
|
146
|
+
font-weight: 600;
|
|
147
|
+
margin-top: 1.67em;
|
|
148
|
+
margin-bottom: 1.67em;
|
|
149
|
+
line-height: 1.5;
|
|
150
|
+
}
|
|
151
|
+
.rich-text-editor h6 {
|
|
152
|
+
font-size: 0.67em;
|
|
153
|
+
font-weight: 600;
|
|
154
|
+
margin-top: 2.33em;
|
|
155
|
+
margin-bottom: 2.33em;
|
|
156
|
+
line-height: 1.6;
|
|
157
|
+
}
|
|
158
|
+
.rich-text-editor p {
|
|
159
|
+
margin-top: 0;
|
|
160
|
+
margin-bottom: 1em;
|
|
161
|
+
line-height: 1.6;
|
|
162
|
+
}
|
|
163
|
+
.rich-text-editor strong,
|
|
164
|
+
.rich-text-editor b {
|
|
165
|
+
font-weight: 700;
|
|
166
|
+
}
|
|
167
|
+
.rich-text-editor em,
|
|
168
|
+
.rich-text-editor i {
|
|
169
|
+
font-style: italic;
|
|
170
|
+
}
|
|
171
|
+
.rich-text-editor s,
|
|
172
|
+
.rich-text-editor del,
|
|
173
|
+
.rich-text-editor strike {
|
|
174
|
+
text-decoration: line-through;
|
|
175
|
+
}
|
|
176
|
+
.rich-text-editor ul {
|
|
177
|
+
list-style-type: disc;
|
|
178
|
+
margin-top: 0;
|
|
179
|
+
margin-bottom: 1em;
|
|
180
|
+
padding-left: 2em;
|
|
181
|
+
}
|
|
182
|
+
.rich-text-editor ul ul {
|
|
183
|
+
list-style-type: circle;
|
|
184
|
+
}
|
|
185
|
+
.rich-text-editor ul ul ul {
|
|
186
|
+
list-style-type: square;
|
|
187
|
+
}
|
|
188
|
+
.rich-text-editor ol {
|
|
189
|
+
list-style-type: decimal;
|
|
190
|
+
margin-top: 0;
|
|
191
|
+
margin-bottom: 1em;
|
|
192
|
+
padding-left: 2em;
|
|
193
|
+
}
|
|
194
|
+
.rich-text-editor li {
|
|
195
|
+
margin-bottom: 0.25em;
|
|
196
|
+
line-height: 1.6;
|
|
197
|
+
}
|
|
198
|
+
.rich-text-editor li > p {
|
|
199
|
+
margin-bottom: 0.25em;
|
|
200
|
+
}
|
|
201
|
+
.rich-text-editor li:last-child {
|
|
202
|
+
margin-bottom: 0;
|
|
203
|
+
}
|
|
204
|
+
.rich-text-editor blockquote {
|
|
205
|
+
border-left: 4px solid hsl(var(--border));
|
|
206
|
+
margin: 1em 0;
|
|
207
|
+
padding-left: 1em;
|
|
208
|
+
font-style: italic;
|
|
209
|
+
color: hsl(var(--muted-foreground));
|
|
210
|
+
}
|
|
211
|
+
.rich-text-editor blockquote p {
|
|
212
|
+
margin-bottom: 0.5em;
|
|
213
|
+
}
|
|
214
|
+
.rich-text-editor blockquote p:last-child {
|
|
215
|
+
margin-bottom: 0;
|
|
216
|
+
}
|
|
217
|
+
.rich-text-editor a {
|
|
218
|
+
color: hsl(var(--primary));
|
|
219
|
+
text-decoration: underline;
|
|
220
|
+
text-underline-offset: 2px;
|
|
221
|
+
cursor: pointer;
|
|
222
|
+
}
|
|
223
|
+
.rich-text-editor a:hover {
|
|
224
|
+
opacity: 0.8;
|
|
225
|
+
}
|
|
226
|
+
.rich-text-editor img,
|
|
227
|
+
.rich-text-editor .rich-text-image {
|
|
228
|
+
max-width: 100%;
|
|
229
|
+
height: auto;
|
|
230
|
+
display: inline-block;
|
|
231
|
+
margin: 0.5em 0;
|
|
232
|
+
border-radius: 4px;
|
|
233
|
+
}
|
|
234
|
+
.rich-text-editor img.ProseMirror-selectednode,
|
|
235
|
+
.rich-text-editor .rich-text-image.ProseMirror-selectednode {
|
|
236
|
+
outline: 2px solid hsl(var(--primary));
|
|
237
|
+
outline-offset: 2px;
|
|
238
|
+
}
|
|
239
|
+
.rich-text-editor table {
|
|
240
|
+
border-collapse: separate;
|
|
241
|
+
border-spacing: 0;
|
|
242
|
+
table-layout: auto;
|
|
243
|
+
width: 100%;
|
|
244
|
+
margin: 1em 0;
|
|
245
|
+
overflow: hidden;
|
|
246
|
+
border: 2px solid var(--color-muted);
|
|
247
|
+
border-radius: 6px;
|
|
248
|
+
}
|
|
249
|
+
.rich-text-editor table colgroup,
|
|
250
|
+
.rich-text-editor table col {
|
|
251
|
+
display: none;
|
|
252
|
+
}
|
|
253
|
+
.rich-text-editor table td,
|
|
254
|
+
.rich-text-editor table th,
|
|
255
|
+
.rich-text-editor .rich-text-table-cell,
|
|
256
|
+
.rich-text-editor .rich-text-table-header {
|
|
257
|
+
min-width: 1em;
|
|
258
|
+
border-right: 1px solid var(--color-muted);
|
|
259
|
+
border-bottom: 1px solid var(--color-muted);
|
|
260
|
+
padding: 8px 12px;
|
|
261
|
+
vertical-align: top;
|
|
262
|
+
box-sizing: border-box;
|
|
263
|
+
position: relative;
|
|
264
|
+
background-color: hsl(var(--background));
|
|
265
|
+
}
|
|
266
|
+
.rich-text-editor table td:last-child,
|
|
267
|
+
.rich-text-editor table th:last-child {
|
|
268
|
+
border-right: none;
|
|
269
|
+
}
|
|
270
|
+
.rich-text-editor table tr:last-child td,
|
|
271
|
+
.rich-text-editor table tr:last-child th {
|
|
272
|
+
border-bottom: none;
|
|
273
|
+
}
|
|
274
|
+
.rich-text-editor table th,
|
|
275
|
+
.rich-text-editor .rich-text-table-header {
|
|
276
|
+
font-weight: 600;
|
|
277
|
+
text-align: left;
|
|
278
|
+
background-color: hsl(var(--muted));
|
|
279
|
+
}
|
|
280
|
+
.rich-text-editor table .selectedCell {
|
|
281
|
+
background-color: hsl(var(--accent));
|
|
282
|
+
}
|
|
283
|
+
.rich-text-editor table .column-resize-handle {
|
|
284
|
+
position: absolute;
|
|
285
|
+
right: -2px;
|
|
286
|
+
top: 0;
|
|
287
|
+
bottom: 0;
|
|
288
|
+
width: 4px;
|
|
289
|
+
background-color: hsl(var(--primary));
|
|
290
|
+
pointer-events: none;
|
|
291
|
+
}
|
|
292
|
+
.rich-text-editor table p {
|
|
293
|
+
margin: 0;
|
|
294
|
+
}
|
|
295
|
+
.rich-text-editor .tableWrapper {
|
|
296
|
+
overflow-x: auto;
|
|
297
|
+
}
|
|
298
|
+
.rich-text-editor .resize-cursor {
|
|
299
|
+
cursor: ew-resize;
|
|
300
|
+
cursor: col-resize;
|
|
301
|
+
}
|
|
302
|
+
.rich-text-editor code {
|
|
303
|
+
background-color: hsl(var(--muted));
|
|
304
|
+
border-radius: 3px;
|
|
305
|
+
font-family: 'Courier New', Courier, monospace;
|
|
306
|
+
padding: 0.2em 0.4em;
|
|
307
|
+
font-size: 0.9em;
|
|
308
|
+
}
|
|
309
|
+
.rich-text-editor pre {
|
|
310
|
+
background-color: hsl(var(--muted));
|
|
311
|
+
border-radius: 6px;
|
|
312
|
+
padding: 1em;
|
|
313
|
+
overflow-x: auto;
|
|
314
|
+
margin: 1em 0;
|
|
315
|
+
}
|
|
316
|
+
.rich-text-editor pre code {
|
|
317
|
+
background-color: transparent;
|
|
318
|
+
padding: 0;
|
|
319
|
+
font-size: 0.9em;
|
|
320
|
+
}
|
|
321
|
+
.rich-text-editor hr {
|
|
322
|
+
border: none;
|
|
323
|
+
border-top: 1px solid hsl(var(--border));
|
|
324
|
+
margin: 2em 0;
|
|
325
|
+
}
|
|
326
|
+
.rich-text-editor:focus {
|
|
327
|
+
outline: none;
|
|
328
|
+
}
|
|
329
|
+
.rich-text-editor > *:first-child {
|
|
330
|
+
margin-top: 0;
|
|
331
|
+
}
|
|
332
|
+
.rich-text-editor > *:last-child {
|
|
333
|
+
margin-bottom: 0;
|
|
334
|
+
}
|
|
335
|
+
`}</style>
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Editor } from '@tiptap/react';
|
|
2
|
+
import { Trash2Icon } from 'lucide-react';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { Button } from '../../ui/button.js';
|
|
5
|
+
|
|
6
|
+
export interface TableDeleteMenuProps {
|
|
7
|
+
editor: Editor | null;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function TableDeleteMenu({ editor, disabled }: Readonly<TableDeleteMenuProps>) {
|
|
12
|
+
const [tableRect, setTableRect] = useState<DOMRect | null>(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!editor) return;
|
|
16
|
+
|
|
17
|
+
const updateTablePosition = () => {
|
|
18
|
+
if (!editor.isActive('table')) {
|
|
19
|
+
setTableRect(null);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const { selection } = editor.state;
|
|
25
|
+
const { $from } = selection;
|
|
26
|
+
|
|
27
|
+
// Find the table cell node
|
|
28
|
+
let cellPos = null;
|
|
29
|
+
|
|
30
|
+
for (let depth = $from.depth; depth >= 0; depth--) {
|
|
31
|
+
const node = $from.node(depth);
|
|
32
|
+
if (node.type.name === 'tableCell' || node.type.name === 'tableHeader') {
|
|
33
|
+
cellPos = $from.before(depth);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (cellPos === null) {
|
|
39
|
+
setTableRect(null);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Get DOM elements
|
|
44
|
+
const cellDOMNode = editor.view.nodeDOM(cellPos) as HTMLElement;
|
|
45
|
+
const tableDOMNode = cellDOMNode?.closest('table');
|
|
46
|
+
|
|
47
|
+
if (!tableDOMNode) {
|
|
48
|
+
setTableRect(null);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setTableRect(tableDOMNode.getBoundingClientRect());
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn('Error calculating table position:', error);
|
|
55
|
+
setTableRect(null);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Update on selection change
|
|
60
|
+
const handleSelectionUpdate = () => {
|
|
61
|
+
updateTablePosition();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
editor.on('selectionUpdate', handleSelectionUpdate);
|
|
65
|
+
editor.on('transaction', handleSelectionUpdate);
|
|
66
|
+
|
|
67
|
+
// Initial update
|
|
68
|
+
updateTablePosition();
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
editor.off('selectionUpdate', handleSelectionUpdate);
|
|
72
|
+
editor.off('transaction', handleSelectionUpdate);
|
|
73
|
+
};
|
|
74
|
+
}, [editor]);
|
|
75
|
+
|
|
76
|
+
if (!tableRect || disabled || !editor) return null;
|
|
77
|
+
|
|
78
|
+
// Position at bottom right of table
|
|
79
|
+
const iconTop = tableRect.bottom - 30;
|
|
80
|
+
const iconLeft = tableRect.right - 30;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
style={{
|
|
85
|
+
position: 'fixed',
|
|
86
|
+
top: iconTop,
|
|
87
|
+
left: iconLeft,
|
|
88
|
+
zIndex: 1000,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<Button
|
|
92
|
+
type="button"
|
|
93
|
+
variant="outline"
|
|
94
|
+
size="sm"
|
|
95
|
+
onClick={() => editor.chain().focus().deleteTable().run()}
|
|
96
|
+
disabled={disabled || !editor.can().deleteTable()}
|
|
97
|
+
className="h-6 w-6 p-0 bg-background shadow-md hover:bg-destructive/10 text-destructive hover:text-destructive"
|
|
98
|
+
title="Delete Table"
|
|
99
|
+
>
|
|
100
|
+
<Trash2Icon className="h-3 w-3" />
|
|
101
|
+
</Button>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
2
|
+
import { Editor } from '@tiptap/react';
|
|
3
|
+
import { MoreHorizontalIcon, MoreVerticalIcon } from 'lucide-react';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
import { Button } from '../../ui/button.js';
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from '../../ui/dropdown-menu.js';
|
|
13
|
+
|
|
14
|
+
export interface TableEditIconsProps {
|
|
15
|
+
editor: Editor | null;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface TableCellPosition {
|
|
20
|
+
columnIndex: number;
|
|
21
|
+
rowIndex: number;
|
|
22
|
+
cellRect: DOMRect | null;
|
|
23
|
+
tableRect: DOMRect | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function TableEditIcons({ editor, disabled }: Readonly<TableEditIconsProps>) {
|
|
27
|
+
const [cellPosition, setCellPosition] = useState<TableCellPosition | null>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!editor) return;
|
|
31
|
+
|
|
32
|
+
const updateCellPosition = () => {
|
|
33
|
+
if (!editor.isActive('table')) {
|
|
34
|
+
setCellPosition(null);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const { selection } = editor.state;
|
|
40
|
+
const { $from } = selection;
|
|
41
|
+
|
|
42
|
+
// Find the table cell node
|
|
43
|
+
let cellNode = null;
|
|
44
|
+
let cellPos = null;
|
|
45
|
+
|
|
46
|
+
for (let depth = $from.depth; depth >= 0; depth--) {
|
|
47
|
+
const node = $from.node(depth);
|
|
48
|
+
if (node.type.name === 'tableCell' || node.type.name === 'tableHeader') {
|
|
49
|
+
cellNode = node;
|
|
50
|
+
cellPos = $from.before(depth);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!cellNode || cellPos === null) {
|
|
56
|
+
setCellPosition(null);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Get DOM elements
|
|
61
|
+
const cellDOMNode = editor.view.nodeDOM(cellPos) as HTMLElement;
|
|
62
|
+
const tableDOMNode = cellDOMNode?.closest('table');
|
|
63
|
+
|
|
64
|
+
if (!cellDOMNode || !tableDOMNode) {
|
|
65
|
+
setCellPosition(null);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Calculate column and row indices
|
|
70
|
+
const row = cellDOMNode.closest('tr');
|
|
71
|
+
const columnIndex = Array.from(row?.children || []).indexOf(cellDOMNode);
|
|
72
|
+
const rowIndex = Array.from(tableDOMNode.querySelectorAll('tr')).indexOf(row!);
|
|
73
|
+
|
|
74
|
+
setCellPosition({
|
|
75
|
+
columnIndex,
|
|
76
|
+
rowIndex,
|
|
77
|
+
cellRect: cellDOMNode.getBoundingClientRect(),
|
|
78
|
+
tableRect: tableDOMNode.getBoundingClientRect(),
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.warn('Error calculating table cell position:', error);
|
|
82
|
+
setCellPosition(null);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Update on selection change
|
|
87
|
+
const handleSelectionUpdate = () => {
|
|
88
|
+
updateCellPosition();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
editor.on('selectionUpdate', handleSelectionUpdate);
|
|
92
|
+
editor.on('transaction', handleSelectionUpdate);
|
|
93
|
+
|
|
94
|
+
// Initial update
|
|
95
|
+
updateCellPosition();
|
|
96
|
+
|
|
97
|
+
return () => {
|
|
98
|
+
editor.off('selectionUpdate', handleSelectionUpdate);
|
|
99
|
+
editor.off('transaction', handleSelectionUpdate);
|
|
100
|
+
};
|
|
101
|
+
}, [editor]);
|
|
102
|
+
|
|
103
|
+
if (!cellPosition || disabled) return null;
|
|
104
|
+
|
|
105
|
+
if (!editor) return null;
|
|
106
|
+
|
|
107
|
+
const { cellRect, tableRect } = cellPosition;
|
|
108
|
+
|
|
109
|
+
if (!cellRect || !tableRect) return null;
|
|
110
|
+
|
|
111
|
+
// Calculate positions for edit icons
|
|
112
|
+
const columnIconTop = tableRect.top - 30;
|
|
113
|
+
const columnIconLeft = cellRect.left + cellRect.width / 2 - 12;
|
|
114
|
+
|
|
115
|
+
const rowIconTop = cellRect.top + cellRect.height / 2 - 12;
|
|
116
|
+
const rowIconLeft = tableRect.left - 30;
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<>
|
|
120
|
+
{/* Column edit icon */}
|
|
121
|
+
<div
|
|
122
|
+
style={{
|
|
123
|
+
position: 'fixed',
|
|
124
|
+
top: columnIconTop,
|
|
125
|
+
left: columnIconLeft,
|
|
126
|
+
zIndex: 1000,
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
<DropdownMenu>
|
|
130
|
+
<DropdownMenuTrigger asChild>
|
|
131
|
+
<Button
|
|
132
|
+
type="button"
|
|
133
|
+
variant="ghost"
|
|
134
|
+
size="sm"
|
|
135
|
+
className="h-6 w-6 p-0 bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground border-0 shadow-sm"
|
|
136
|
+
>
|
|
137
|
+
<MoreHorizontalIcon className="h-3 w-3" />
|
|
138
|
+
</Button>
|
|
139
|
+
</DropdownMenuTrigger>
|
|
140
|
+
<DropdownMenuContent align="center">
|
|
141
|
+
<DropdownMenuItem
|
|
142
|
+
onClick={() => editor.chain().focus().addColumnBefore().run()}
|
|
143
|
+
disabled={!editor.can().addColumnBefore()}
|
|
144
|
+
>
|
|
145
|
+
<Trans>Add column before</Trans>
|
|
146
|
+
</DropdownMenuItem>
|
|
147
|
+
<DropdownMenuItem
|
|
148
|
+
onClick={() => editor.chain().focus().addColumnAfter().run()}
|
|
149
|
+
disabled={!editor.can().addColumnAfter()}
|
|
150
|
+
>
|
|
151
|
+
<Trans>Add column after</Trans>
|
|
152
|
+
</DropdownMenuItem>
|
|
153
|
+
<DropdownMenuSeparator />
|
|
154
|
+
<DropdownMenuItem
|
|
155
|
+
onClick={() => editor.chain().focus().toggleHeaderColumn().run()}
|
|
156
|
+
disabled={!editor.can().toggleHeaderColumn()}
|
|
157
|
+
>
|
|
158
|
+
<Trans>Toggle header column</Trans>
|
|
159
|
+
</DropdownMenuItem>
|
|
160
|
+
<DropdownMenuSeparator />
|
|
161
|
+
<DropdownMenuItem
|
|
162
|
+
onClick={() => editor.chain().focus().deleteColumn().run()}
|
|
163
|
+
disabled={!editor.can().deleteColumn()}
|
|
164
|
+
className="text-destructive focus:text-destructive"
|
|
165
|
+
>
|
|
166
|
+
<Trans>Delete column</Trans>
|
|
167
|
+
</DropdownMenuItem>
|
|
168
|
+
</DropdownMenuContent>
|
|
169
|
+
</DropdownMenu>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{/* Row edit icon */}
|
|
173
|
+
<div
|
|
174
|
+
style={{
|
|
175
|
+
position: 'fixed',
|
|
176
|
+
top: rowIconTop,
|
|
177
|
+
left: rowIconLeft,
|
|
178
|
+
zIndex: 1000,
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<DropdownMenu>
|
|
182
|
+
<DropdownMenuTrigger asChild>
|
|
183
|
+
<Button
|
|
184
|
+
type="button"
|
|
185
|
+
variant="ghost"
|
|
186
|
+
size="sm"
|
|
187
|
+
className="h-6 w-6 p-0 bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground border-0 shadow-sm"
|
|
188
|
+
>
|
|
189
|
+
<MoreVerticalIcon className="h-3 w-3" />
|
|
190
|
+
</Button>
|
|
191
|
+
</DropdownMenuTrigger>
|
|
192
|
+
<DropdownMenuContent align="center">
|
|
193
|
+
<DropdownMenuItem
|
|
194
|
+
onClick={() => editor.chain().focus().addRowBefore().run()}
|
|
195
|
+
disabled={!editor.can().addRowBefore()}
|
|
196
|
+
>
|
|
197
|
+
<Trans>Add row before</Trans>
|
|
198
|
+
</DropdownMenuItem>
|
|
199
|
+
<DropdownMenuItem
|
|
200
|
+
onClick={() => editor.chain().focus().addRowAfter().run()}
|
|
201
|
+
disabled={!editor.can().addRowAfter()}
|
|
202
|
+
>
|
|
203
|
+
<Trans>Add row after</Trans>
|
|
204
|
+
</DropdownMenuItem>
|
|
205
|
+
<DropdownMenuSeparator />
|
|
206
|
+
<DropdownMenuItem
|
|
207
|
+
onClick={() => editor.chain().focus().toggleHeaderRow().run()}
|
|
208
|
+
disabled={!editor.can().toggleHeaderRow()}
|
|
209
|
+
>
|
|
210
|
+
<Trans>Toggle header row</Trans>
|
|
211
|
+
</DropdownMenuItem>
|
|
212
|
+
<DropdownMenuSeparator />
|
|
213
|
+
<DropdownMenuItem
|
|
214
|
+
onClick={() => editor.chain().focus().deleteRow().run()}
|
|
215
|
+
disabled={!editor.can().deleteRow()}
|
|
216
|
+
className="text-destructive focus:text-destructive"
|
|
217
|
+
>
|
|
218
|
+
<Trans>Delete row</Trans>
|
|
219
|
+
</DropdownMenuItem>
|
|
220
|
+
</DropdownMenuContent>
|
|
221
|
+
</DropdownMenu>
|
|
222
|
+
</div>
|
|
223
|
+
</>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { configArgDefinitionFragment } from '@/vdb/graphql/fragments.js';
|
|
2
|
+
|
|
1
3
|
import { graphql } from './graphql.js';
|
|
2
4
|
|
|
3
5
|
export const duplicateEntityDocument = graphql(`
|
|
@@ -16,3 +18,20 @@ export const duplicateEntityDocument = graphql(`
|
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
`);
|
|
21
|
+
|
|
22
|
+
export const getEntityDuplicatorsDocument = graphql(
|
|
23
|
+
`
|
|
24
|
+
query GetEntityDuplicators {
|
|
25
|
+
entityDuplicators {
|
|
26
|
+
code
|
|
27
|
+
description
|
|
28
|
+
requiresPermission
|
|
29
|
+
forEntities
|
|
30
|
+
args {
|
|
31
|
+
...ConfigArgDefinition
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
`,
|
|
36
|
+
[configArgDefinitionFragment],
|
|
37
|
+
);
|