@vendure/dashboard 3.4.3-master-202509120226 → 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/app/routes/_authenticated/_collections/collections.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_collections/collections.tsx +21 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_products/components/product-variants-table.tsx +30 -2
- package/src/app/routes/_authenticated/_products/products.graphql.ts +25 -16
- 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
- package/src/lib/hooks/use-floating-bulk-actions.ts +3 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
2
|
+
import { SetImageOptions } from '@tiptap/extension-image';
|
|
3
|
+
import { Editor } from '@tiptap/react';
|
|
4
|
+
import { ImageIcon, PaperclipIcon } from 'lucide-react';
|
|
5
|
+
import { useEffect, useState } from 'react';
|
|
6
|
+
import { Button } from '../../ui/button.js';
|
|
7
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog.js';
|
|
8
|
+
import { Input } from '../../ui/input.js';
|
|
9
|
+
import { Label } from '../../ui/label.js';
|
|
10
|
+
import { Asset } from '../asset/asset-gallery.js';
|
|
11
|
+
import { AssetPickerDialog } from '../asset/asset-picker-dialog.js';
|
|
12
|
+
|
|
13
|
+
export interface ImageDialogProps {
|
|
14
|
+
editor: Editor;
|
|
15
|
+
isOpen: boolean;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ImageDialog({ editor, isOpen, onClose }: Readonly<ImageDialogProps>) {
|
|
20
|
+
const [src, setSrc] = useState('');
|
|
21
|
+
const [alt, setAlt] = useState('');
|
|
22
|
+
const [title, setTitle] = useState('');
|
|
23
|
+
const [width, setWidth] = useState('');
|
|
24
|
+
const [height, setHeight] = useState('');
|
|
25
|
+
const [assetPickerOpen, setAssetPickerOpen] = useState(false);
|
|
26
|
+
const [previewUrl, setPreviewUrl] = useState('');
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (isOpen) {
|
|
30
|
+
// Get current image attributes if editing existing image
|
|
31
|
+
const {
|
|
32
|
+
src: currentSrc,
|
|
33
|
+
alt: currentAlt,
|
|
34
|
+
title: currentTitle,
|
|
35
|
+
width: currentWidth,
|
|
36
|
+
height: currentHeight,
|
|
37
|
+
} = editor.getAttributes('image');
|
|
38
|
+
|
|
39
|
+
setSrc(currentSrc || '');
|
|
40
|
+
setAlt(currentAlt || '');
|
|
41
|
+
setTitle(currentTitle || '');
|
|
42
|
+
setWidth(currentWidth || '');
|
|
43
|
+
setHeight(currentHeight || '');
|
|
44
|
+
setPreviewUrl(currentSrc || '');
|
|
45
|
+
}
|
|
46
|
+
}, [isOpen, editor]);
|
|
47
|
+
|
|
48
|
+
const handleInsertImage = () => {
|
|
49
|
+
if (!src) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const attrs: SetImageOptions = {
|
|
54
|
+
src,
|
|
55
|
+
alt: alt || undefined,
|
|
56
|
+
title: title || undefined,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Only add width/height if they are valid numbers
|
|
60
|
+
if (width && !isNaN(Number(width))) {
|
|
61
|
+
attrs.width = Number(width);
|
|
62
|
+
}
|
|
63
|
+
if (height && !isNaN(Number(height))) {
|
|
64
|
+
attrs.height = Number(height);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
editor.chain().focus().setImage(attrs).run();
|
|
68
|
+
|
|
69
|
+
handleClose();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleClose = () => {
|
|
73
|
+
setSrc('');
|
|
74
|
+
setAlt('');
|
|
75
|
+
setTitle('');
|
|
76
|
+
setWidth('');
|
|
77
|
+
setHeight('');
|
|
78
|
+
setPreviewUrl('');
|
|
79
|
+
onClose();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handleAssetSelect = (assets: Asset[]) => {
|
|
83
|
+
if (assets.length > 0) {
|
|
84
|
+
const asset = assets[0];
|
|
85
|
+
setSrc(asset.source);
|
|
86
|
+
setPreviewUrl(asset.preview);
|
|
87
|
+
// Set width and height from asset if available
|
|
88
|
+
if (asset.width) {
|
|
89
|
+
setWidth(asset.width.toString());
|
|
90
|
+
}
|
|
91
|
+
if (asset.height) {
|
|
92
|
+
setHeight(asset.height.toString());
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
setAssetPickerOpen(false);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const isEditing = editor.isActive('image');
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
<Dialog open={isOpen} onOpenChange={open => !open && handleClose()}>
|
|
103
|
+
<DialogContent className="sm:max-w-[525px]">
|
|
104
|
+
<DialogHeader>
|
|
105
|
+
<DialogTitle>
|
|
106
|
+
{isEditing ? <Trans>Edit image</Trans> : <Trans>Insert image</Trans>}
|
|
107
|
+
</DialogTitle>
|
|
108
|
+
</DialogHeader>
|
|
109
|
+
<div className="grid gap-4 py-4">
|
|
110
|
+
<div className="flex items-center justify-center">
|
|
111
|
+
{previewUrl ? (
|
|
112
|
+
<img
|
|
113
|
+
src={previewUrl}
|
|
114
|
+
alt={alt || 'Preview'}
|
|
115
|
+
className="max-w-[200px] max-h-[200px] object-contain border rounded"
|
|
116
|
+
/>
|
|
117
|
+
) : (
|
|
118
|
+
<div className="w-[200px] h-[200px] border rounded flex items-center justify-center bg-muted">
|
|
119
|
+
<ImageIcon className="w-16 h-16 text-muted-foreground" />
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div className="flex items-center justify-center">
|
|
125
|
+
<Button
|
|
126
|
+
type="button"
|
|
127
|
+
variant="outline"
|
|
128
|
+
size="sm"
|
|
129
|
+
onClick={() => setAssetPickerOpen(true)}
|
|
130
|
+
>
|
|
131
|
+
<PaperclipIcon className="w-4 h-4 mr-2" />
|
|
132
|
+
<Trans>Add asset</Trans>
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="grid gap-2">
|
|
137
|
+
<Label htmlFor="image-source">
|
|
138
|
+
<Trans>Source</Trans>
|
|
139
|
+
</Label>
|
|
140
|
+
<Input
|
|
141
|
+
id="image-source"
|
|
142
|
+
value={src}
|
|
143
|
+
onChange={e => {
|
|
144
|
+
setSrc(e.target.value);
|
|
145
|
+
setPreviewUrl(e.target.value);
|
|
146
|
+
}}
|
|
147
|
+
placeholder="https://example.com/image.jpg"
|
|
148
|
+
autoFocus
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div className="grid gap-2">
|
|
153
|
+
<Label htmlFor="image-title">
|
|
154
|
+
<Trans>Title</Trans>
|
|
155
|
+
</Label>
|
|
156
|
+
<Input
|
|
157
|
+
id="image-title"
|
|
158
|
+
value={title}
|
|
159
|
+
onChange={e => setTitle(e.target.value)}
|
|
160
|
+
placeholder=""
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div className="grid gap-2">
|
|
165
|
+
<Label htmlFor="image-alt">
|
|
166
|
+
<Trans>Description (alt)</Trans>
|
|
167
|
+
</Label>
|
|
168
|
+
<Input
|
|
169
|
+
id="image-alt"
|
|
170
|
+
value={alt}
|
|
171
|
+
onChange={e => setAlt(e.target.value)}
|
|
172
|
+
placeholder=""
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div className="grid grid-cols-2 gap-4">
|
|
177
|
+
<div className="grid gap-2">
|
|
178
|
+
<Label htmlFor="image-width">
|
|
179
|
+
<Trans>Width</Trans>
|
|
180
|
+
</Label>
|
|
181
|
+
<Input
|
|
182
|
+
id="image-width"
|
|
183
|
+
type="number"
|
|
184
|
+
value={width}
|
|
185
|
+
onChange={e => setWidth(e.target.value)}
|
|
186
|
+
placeholder="auto"
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
<div className="grid gap-2">
|
|
190
|
+
<Label htmlFor="image-height">
|
|
191
|
+
<Trans>Height</Trans>
|
|
192
|
+
</Label>
|
|
193
|
+
<Input
|
|
194
|
+
id="image-height"
|
|
195
|
+
type="number"
|
|
196
|
+
value={height}
|
|
197
|
+
onChange={e => setHeight(e.target.value)}
|
|
198
|
+
placeholder="auto"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
<DialogFooter>
|
|
204
|
+
<Button type="button" variant="outline" onClick={handleClose}>
|
|
205
|
+
<Trans>Cancel</Trans>
|
|
206
|
+
</Button>
|
|
207
|
+
<Button type="button" onClick={handleInsertImage} disabled={!src}>
|
|
208
|
+
<Trans>Insert image</Trans>
|
|
209
|
+
</Button>
|
|
210
|
+
</DialogFooter>
|
|
211
|
+
</DialogContent>
|
|
212
|
+
</Dialog>
|
|
213
|
+
|
|
214
|
+
<AssetPickerDialog
|
|
215
|
+
open={assetPickerOpen}
|
|
216
|
+
onClose={() => setAssetPickerOpen(false)}
|
|
217
|
+
onSelect={handleAssetSelect}
|
|
218
|
+
multiSelect={false}
|
|
219
|
+
title="Select asset"
|
|
220
|
+
/>
|
|
221
|
+
</>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
2
|
+
import { Editor } from '@tiptap/react';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { Button } from '../../ui/button.js';
|
|
5
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog.js';
|
|
6
|
+
import { Input } from '../../ui/input.js';
|
|
7
|
+
import { Label } from '../../ui/label.js';
|
|
8
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select.js';
|
|
9
|
+
|
|
10
|
+
export interface LinkDialogProps {
|
|
11
|
+
editor: Editor;
|
|
12
|
+
isOpen: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function LinkDialog({ editor, isOpen, onClose }: Readonly<LinkDialogProps>) {
|
|
17
|
+
const [href, setHref] = useState('');
|
|
18
|
+
const [title, setTitle] = useState('');
|
|
19
|
+
const [target, setTarget] = useState<'_self' | '_blank'>('_self');
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (isOpen) {
|
|
23
|
+
// Get current link attributes if editing existing link
|
|
24
|
+
const {
|
|
25
|
+
href: currentHref,
|
|
26
|
+
target: currentTarget,
|
|
27
|
+
title: currentTitle,
|
|
28
|
+
} = editor.getAttributes('link');
|
|
29
|
+
|
|
30
|
+
setHref(currentHref || '');
|
|
31
|
+
setTitle(currentTitle || '');
|
|
32
|
+
setTarget(currentTarget === '_blank' ? '_blank' : '_self');
|
|
33
|
+
|
|
34
|
+
// If text is selected but no link, use selection as initial href if it looks like a URL
|
|
35
|
+
if (!currentHref) {
|
|
36
|
+
const { from, to } = editor.state.selection;
|
|
37
|
+
const selectedText = editor.state.doc.textBetween(from, to);
|
|
38
|
+
if (selectedText && (selectedText.startsWith('http') || selectedText.startsWith('www'))) {
|
|
39
|
+
setHref(selectedText);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}, [isOpen, editor]);
|
|
44
|
+
|
|
45
|
+
const handleSetLink = () => {
|
|
46
|
+
if (!href) {
|
|
47
|
+
// Remove link if href is empty
|
|
48
|
+
editor.chain().focus().unsetLink().run();
|
|
49
|
+
} else {
|
|
50
|
+
// Set link with all attributes
|
|
51
|
+
editor
|
|
52
|
+
.chain()
|
|
53
|
+
.focus()
|
|
54
|
+
.extendMarkRange('link')
|
|
55
|
+
.setLink({
|
|
56
|
+
href,
|
|
57
|
+
target,
|
|
58
|
+
rel: target === '_blank' ? 'noopener noreferrer' : undefined,
|
|
59
|
+
})
|
|
60
|
+
.run();
|
|
61
|
+
}
|
|
62
|
+
handleClose();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleClose = () => {
|
|
66
|
+
setHref('');
|
|
67
|
+
setTitle('');
|
|
68
|
+
setTarget('_self');
|
|
69
|
+
onClose();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleRemoveLink = () => {
|
|
73
|
+
editor.chain().focus().unsetLink().run();
|
|
74
|
+
handleClose();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const isEditing = editor.isActive('link');
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Dialog open={isOpen} onOpenChange={open => !open && handleClose()}>
|
|
81
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
82
|
+
<DialogHeader>
|
|
83
|
+
<DialogTitle>
|
|
84
|
+
{isEditing ? <Trans>Edit link</Trans> : <Trans>Insert link</Trans>}
|
|
85
|
+
</DialogTitle>
|
|
86
|
+
</DialogHeader>
|
|
87
|
+
<div className="grid gap-4 py-4">
|
|
88
|
+
<div className="grid grid-cols-4 items-center gap-4">
|
|
89
|
+
<Label htmlFor="link-href" className="text-right">
|
|
90
|
+
<Trans>Link href</Trans>
|
|
91
|
+
</Label>
|
|
92
|
+
<Input
|
|
93
|
+
id="link-href"
|
|
94
|
+
value={href}
|
|
95
|
+
onChange={e => setHref(e.target.value)}
|
|
96
|
+
placeholder="https://example.com"
|
|
97
|
+
className="col-span-3"
|
|
98
|
+
autoFocus
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="grid grid-cols-4 items-center gap-4">
|
|
102
|
+
<Label htmlFor="link-title" className="text-right">
|
|
103
|
+
<Trans>Link title</Trans>
|
|
104
|
+
</Label>
|
|
105
|
+
<Input
|
|
106
|
+
id="link-title"
|
|
107
|
+
value={title}
|
|
108
|
+
onChange={e => setTitle(e.target.value)}
|
|
109
|
+
placeholder=""
|
|
110
|
+
className="col-span-3"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
<div className="grid grid-cols-4 items-center gap-4">
|
|
114
|
+
<Label htmlFor="link-target" className="text-right">
|
|
115
|
+
<Trans>Link target</Trans>
|
|
116
|
+
</Label>
|
|
117
|
+
<Select
|
|
118
|
+
value={target}
|
|
119
|
+
onValueChange={value => setTarget(value as '_self' | '_blank')}
|
|
120
|
+
>
|
|
121
|
+
<SelectTrigger className="col-span-3">
|
|
122
|
+
<SelectValue />
|
|
123
|
+
</SelectTrigger>
|
|
124
|
+
<SelectContent>
|
|
125
|
+
<SelectItem value="_self">
|
|
126
|
+
<Trans>Same window</Trans>
|
|
127
|
+
</SelectItem>
|
|
128
|
+
<SelectItem value="_blank">
|
|
129
|
+
<Trans>New window</Trans>
|
|
130
|
+
</SelectItem>
|
|
131
|
+
</SelectContent>
|
|
132
|
+
</Select>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<DialogFooter>
|
|
136
|
+
{isEditing && (
|
|
137
|
+
<Button type="button" variant="destructive" onClick={handleRemoveLink}>
|
|
138
|
+
<Trans>Remove link</Trans>
|
|
139
|
+
</Button>
|
|
140
|
+
)}
|
|
141
|
+
<Button type="button" variant="outline" onClick={handleClose}>
|
|
142
|
+
<Trans>Cancel</Trans>
|
|
143
|
+
</Button>
|
|
144
|
+
<Button type="button" onClick={handleSetLink}>
|
|
145
|
+
<Trans>Set link</Trans>
|
|
146
|
+
</Button>
|
|
147
|
+
</DialogFooter>
|
|
148
|
+
</DialogContent>
|
|
149
|
+
</Dialog>
|
|
150
|
+
);
|
|
151
|
+
}
|