@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.
@@ -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
+ }