@vendure/dashboard 3.3.8-master-202507260236 → 3.3.8-master-202507300243

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.
Files changed (35) hide show
  1. package/package.json +4 -4
  2. package/src/app/routes/_authenticated/_collections/components/collection-contents-preview-table.tsx +1 -1
  3. package/src/app/routes/_authenticated/_collections/components/collection-filters-selector.tsx +11 -78
  4. package/src/app/routes/_authenticated/_payment-methods/components/payment-eligibility-checker-selector.tsx +11 -81
  5. package/src/app/routes/_authenticated/_payment-methods/components/payment-handler-selector.tsx +10 -77
  6. package/src/app/routes/_authenticated/_promotions/components/promotion-actions-selector.tsx +12 -87
  7. package/src/app/routes/_authenticated/_promotions/components/promotion-conditions-selector.tsx +12 -87
  8. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-calculator-selector.tsx +10 -80
  9. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-eligibility-checker-selector.tsx +10 -79
  10. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +8 -6
  11. package/src/lib/components/data-input/combination-mode-input.tsx +52 -0
  12. package/src/lib/components/data-input/configurable-operation-list-input.tsx +433 -0
  13. package/src/lib/components/data-input/custom-field-list-input.tsx +297 -0
  14. package/src/lib/components/data-input/datetime-input.tsx +5 -2
  15. package/src/lib/components/data-input/default-relation-input.tsx +599 -0
  16. package/src/lib/components/data-input/index.ts +6 -0
  17. package/src/lib/components/data-input/product-multi-selector.tsx +426 -0
  18. package/src/lib/components/data-input/relation-selector.tsx +7 -6
  19. package/src/lib/components/data-input/select-with-options.tsx +84 -0
  20. package/src/lib/components/data-input/struct-form-input.tsx +324 -0
  21. package/src/lib/components/shared/configurable-operation-arg-input.tsx +365 -21
  22. package/src/lib/components/shared/configurable-operation-input.tsx +81 -41
  23. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +260 -0
  24. package/src/lib/components/shared/configurable-operation-selector.tsx +156 -0
  25. package/src/lib/components/shared/custom-fields-form.tsx +207 -36
  26. package/src/lib/components/shared/multi-select.tsx +1 -1
  27. package/src/lib/components/ui/form.tsx +4 -4
  28. package/src/lib/framework/extension-api/input-component-extensions.tsx +5 -1
  29. package/src/lib/framework/form-engine/form-schema-tools.spec.ts +472 -0
  30. package/src/lib/framework/form-engine/form-schema-tools.ts +340 -5
  31. package/src/lib/framework/form-engine/use-generated-form.tsx +24 -8
  32. package/src/lib/framework/form-engine/utils.ts +3 -9
  33. package/src/lib/framework/layout-engine/page-layout.tsx +11 -3
  34. package/src/lib/framework/page/use-detail-page.ts +3 -3
  35. package/src/lib/lib/utils.ts +26 -24
@@ -1,21 +1,7 @@
1
- import { ConfigurableOperationInput } from '@/vdb/components/shared/configurable-operation-input.js';
2
- import { Button } from '@/vdb/components/ui/button.js';
3
- import {
4
- DropdownMenu,
5
- DropdownMenuContent,
6
- DropdownMenuItem,
7
- DropdownMenuTrigger,
8
- } from '@/vdb/components/ui/dropdown-menu.js';
9
- import { api } from '@/vdb/graphql/api.js';
10
- import {
11
- configurableOperationDefFragment,
12
- ConfigurableOperationDefFragment,
13
- } from '@/vdb/graphql/fragments.js';
1
+ import { ConfigurableOperationSelector } from '@/vdb/components/shared/configurable-operation-selector.js';
2
+ import { configurableOperationDefFragment } from '@/vdb/graphql/fragments.js';
14
3
  import { graphql } from '@/vdb/graphql/graphql.js';
15
- import { Trans } from '@/vdb/lib/trans.js';
16
- import { useQuery } from '@tanstack/react-query';
17
4
  import { ConfigurableOperationInput as ConfigurableOperationInputType } from '@vendure/common/lib/generated-types';
18
- import { Plus } from 'lucide-react';
19
5
 
20
6
  export const shippingEligibilityCheckersDocument = graphql(
21
7
  `
@@ -37,69 +23,14 @@ export function ShippingEligibilityCheckerSelector({
37
23
  value,
38
24
  onChange,
39
25
  }: ShippingEligibilityCheckerSelectorProps) {
40
- const { data: checkersData } = useQuery({
41
- queryKey: ['shippingEligibilityCheckers'],
42
- queryFn: () => api.query(shippingEligibilityCheckersDocument),
43
- staleTime: 1000 * 60 * 60 * 5,
44
- });
45
-
46
- const checkers = checkersData?.shippingEligibilityCheckers;
47
-
48
- const onCheckerSelected = (checker: ConfigurableOperationDefFragment) => {
49
- const checkerDef = checkers?.find(c => c.code === checker.code);
50
- if (!checkerDef) {
51
- return;
52
- }
53
- onChange({
54
- code: checker.code,
55
- arguments: checkerDef.args.map(arg => ({
56
- name: arg.name,
57
- value: arg.defaultValue != null ? arg.defaultValue.toString() : '',
58
- })),
59
- });
60
- };
61
-
62
- const onOperationValueChange = (newVal: ConfigurableOperationInputType) => {
63
- onChange(newVal);
64
- };
65
-
66
- const onOperationRemove = () => {
67
- onChange(undefined);
68
- };
69
-
70
- const checkerDef = checkers?.find(c => c.code === value?.code);
71
-
72
26
  return (
73
- <div className="flex flex-col gap-2 mt-4">
74
- {value && checkerDef && (
75
- <div className="flex flex-col gap-2">
76
- <ConfigurableOperationInput
77
- operationDefinition={checkerDef}
78
- value={value}
79
- onChange={value => onOperationValueChange(value)}
80
- onRemove={() => onOperationRemove()}
81
- />
82
- </div>
83
- )}
84
- <DropdownMenu>
85
- {!value && (
86
- <DropdownMenuTrigger asChild>
87
- <Button variant="outline">
88
- <Plus />
89
- <Trans context="Add new promotion action">
90
- Select Shipping Eligibility Checker
91
- </Trans>
92
- </Button>
93
- </DropdownMenuTrigger>
94
- )}
95
- <DropdownMenuContent className="w-96">
96
- {checkers?.map(checker => (
97
- <DropdownMenuItem key={checker.code} onClick={() => onCheckerSelected(checker)}>
98
- {checker.description}
99
- </DropdownMenuItem>
100
- ))}
101
- </DropdownMenuContent>
102
- </DropdownMenu>
103
- </div>
27
+ <ConfigurableOperationSelector
28
+ value={value}
29
+ onChange={onChange}
30
+ queryDocument={shippingEligibilityCheckersDocument}
31
+ queryKey="shippingEligibilityCheckers"
32
+ dataPath="shippingEligibilityCheckers"
33
+ buttonText="Select Shipping Eligibility Checker"
34
+ />
104
35
  );
105
36
  }
@@ -130,12 +130,14 @@ function ShippingMethodDetailPage() {
130
130
  render={({ field }) => <Input {...field} />}
131
131
  />
132
132
  </DetailFormGrid>
133
- <TranslatableFormFieldWrapper
134
- control={form.control}
135
- name="description"
136
- label={<Trans>Description</Trans>}
137
- render={({ field }) => <Textarea {...field} />}
138
- />
133
+ <div className="mb-6">
134
+ <TranslatableFormFieldWrapper
135
+ control={form.control}
136
+ name="description"
137
+ label={<Trans>Description</Trans>}
138
+ render={({ field }) => <Textarea {...field} />}
139
+ />
140
+ </div>
139
141
  <DetailFormGrid>
140
142
  <FormFieldWrapper
141
143
  control={form.control}
@@ -0,0 +1,52 @@
1
+ import { DataInputComponent } from '@/vdb/framework/component-registry/component-registry.js';
2
+ import { Trans } from '@/vdb/lib/trans.js';
3
+
4
+ export const CombinationModeInput: DataInputComponent = ({ value, onChange, position, ...props }) => {
5
+ const booleanValue = value === 'true' || value === true;
6
+
7
+ // Only show for items after the first one
8
+ const selectable = position !== undefined && position > 0;
9
+
10
+ const setCombinationModeAnd = () => {
11
+ onChange(true);
12
+ };
13
+
14
+ const setCombinationModeOr = () => {
15
+ onChange(false);
16
+ };
17
+
18
+ if (!selectable) {
19
+ return null;
20
+ }
21
+
22
+ return (
23
+ <div className="flex items-center justify-center -mt-4 -mb-4">
24
+ <div className="bg-muted border px-3 py-1.5 rounded-full flex gap-1.5 text-xs shadow-sm">
25
+ <button
26
+ type="button"
27
+ className={`px-2 py-0.5 rounded-full transition-colors ${
28
+ booleanValue
29
+ ? 'bg-primary text-background'
30
+ : 'text-muted-foreground hover:bg-muted-foreground/10'
31
+ }`}
32
+ onClick={setCombinationModeAnd}
33
+ {...props}
34
+ >
35
+ <Trans>AND</Trans>
36
+ </button>
37
+ <button
38
+ type="button"
39
+ className={`px-2 py-0.5 rounded-full transition-colors ${
40
+ !booleanValue
41
+ ? 'bg-primary text-background'
42
+ : 'text-muted-foreground hover:bg-muted-foreground/10'
43
+ }`}
44
+ onClick={setCombinationModeOr}
45
+ {...props}
46
+ >
47
+ <Trans>OR</Trans>
48
+ </button>
49
+ </div>
50
+ </div>
51
+ );
52
+ };
@@ -0,0 +1,433 @@
1
+ import { ConfigurableOperationDefFragment } from '@/vdb/graphql/fragments.js';
2
+ import { ConfigArgType } from '@vendure/core';
3
+ import { Plus, X } from 'lucide-react';
4
+ import { useState } from 'react';
5
+ import { Button } from '../ui/button.js';
6
+ import { Input } from '../ui/input.js';
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select.js';
8
+ import { Switch } from '../ui/switch.js';
9
+ import { Textarea } from '../ui/textarea.js';
10
+ import { DateTimeInput } from './datetime-input.js';
11
+
12
+ export interface EnhancedListInputProps {
13
+ definition: ConfigurableOperationDefFragment['args'][number];
14
+ value: string;
15
+ onChange: (value: string) => void;
16
+ readOnly?: boolean;
17
+ }
18
+
19
+ /**
20
+ * A dynamic array input component for configurable operation arguments that handle lists of values.
21
+ *
22
+ * This component allows users to add, edit, and remove multiple items from an array-type argument.
23
+ * Each item in the array is rendered using the appropriate input control based on the argument's
24
+ * type and UI configuration (e.g., text input, select dropdown, boolean switch, date picker).
25
+ *
26
+ * The component supports:
27
+ * - Adding new items with appropriate input controls
28
+ * - Editing existing items inline
29
+ * - Removing items from the array
30
+ * - Various data types: string, number, boolean, datetime, currency
31
+ * - Multiple UI components: select, textarea, currency input, etc.
32
+ * - Keyboard shortcuts (Enter to add items)
33
+ * - Read-only mode for display purposes
34
+ *
35
+ * Used primarily in configurable operations (promotions, shipping methods, payment methods)
36
+ * where an argument accepts multiple values, such as a list of product IDs, category codes,
37
+ * or discount amounts.
38
+ *
39
+ * @example
40
+ * // For a promotion condition that accepts multiple product category codes
41
+ * <EnhancedListInput
42
+ * definition={argDefinition}
43
+ * value='["electronics", "books", "clothing"]'
44
+ * onChange={handleChange}
45
+ * />
46
+ */
47
+ export function ConfigurableOperationListInput({
48
+ definition,
49
+ value,
50
+ onChange,
51
+ readOnly,
52
+ }: Readonly<EnhancedListInputProps>) {
53
+ const [newItemValue, setNewItemValue] = useState('');
54
+
55
+ // Parse the current array value
56
+ const arrayValue = parseArrayValue(value);
57
+
58
+ const handleArrayChange = (newArray: string[]) => {
59
+ onChange(JSON.stringify(newArray));
60
+ };
61
+
62
+ const handleAddItem = () => {
63
+ if (newItemValue.trim()) {
64
+ const newArray = [...arrayValue, newItemValue.trim()];
65
+ handleArrayChange(newArray);
66
+ setNewItemValue('');
67
+ }
68
+ };
69
+
70
+ const handleRemoveItem = (index: number) => {
71
+ const newArray = arrayValue.filter((_, i) => i !== index);
72
+ handleArrayChange(newArray);
73
+ };
74
+
75
+ const handleUpdateItem = (index: number, newValue: string) => {
76
+ const newArray = arrayValue.map((item, i) => (i === index ? newValue : item));
77
+ handleArrayChange(newArray);
78
+ };
79
+
80
+ const handleKeyPress = (e: React.KeyboardEvent) => {
81
+ if (e.key === 'Enter' && !e.shiftKey) {
82
+ e.preventDefault();
83
+ handleAddItem();
84
+ }
85
+ };
86
+
87
+ // Render individual item input based on the underlying type
88
+ const renderItemInput = (itemValue: string, index: number) => {
89
+ const argType = definition.type as ConfigArgType;
90
+ const uiComponent = (definition.ui as any)?.component;
91
+
92
+ const commonProps = {
93
+ value: itemValue,
94
+ onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
95
+ handleUpdateItem(index, e.target.value),
96
+ disabled: readOnly,
97
+ };
98
+
99
+ switch (uiComponent) {
100
+ case 'boolean-form-input':
101
+ return (
102
+ <Switch
103
+ checked={itemValue === 'true'}
104
+ onCheckedChange={checked => handleUpdateItem(index, checked.toString())}
105
+ disabled={readOnly}
106
+ />
107
+ );
108
+
109
+ case 'select-form-input': {
110
+ const options = (definition.ui as any)?.options || [];
111
+ return (
112
+ <Select
113
+ value={itemValue}
114
+ onValueChange={val => handleUpdateItem(index, val)}
115
+ disabled={readOnly}
116
+ >
117
+ <SelectTrigger>
118
+ <SelectValue />
119
+ </SelectTrigger>
120
+ <SelectContent>
121
+ {options.map((option: any) => (
122
+ <SelectItem key={option.value} value={option.value}>
123
+ {typeof option.label === 'string'
124
+ ? option.label
125
+ : option.label?.[0]?.value || option.value}
126
+ </SelectItem>
127
+ ))}
128
+ </SelectContent>
129
+ </Select>
130
+ );
131
+ }
132
+ case 'textarea-form-input':
133
+ return (
134
+ <Textarea
135
+ {...commonProps}
136
+ placeholder="Enter text..."
137
+ rows={2}
138
+ className="bg-background"
139
+ />
140
+ );
141
+
142
+ case 'date-form-input':
143
+ return (
144
+ <DateTimeInput
145
+ value={itemValue ? new Date(itemValue) : new Date()}
146
+ onChange={val => handleUpdateItem(index, val.toISOString())}
147
+ disabled={readOnly}
148
+ />
149
+ );
150
+
151
+ case 'number-form-input': {
152
+ const ui = definition.ui as any;
153
+ const isFloat = argType === 'float';
154
+ return (
155
+ <Input
156
+ type="number"
157
+ value={itemValue}
158
+ onChange={e => handleUpdateItem(index, e.target.value)}
159
+ disabled={readOnly}
160
+ min={ui?.min}
161
+ max={ui?.max}
162
+ step={ui?.step || (isFloat ? 0.01 : 1)}
163
+ />
164
+ );
165
+ }
166
+ case 'currency-form-input':
167
+ return (
168
+ <div className="flex items-center">
169
+ <span className="mr-2 text-sm text-muted-foreground">$</span>
170
+ <Input
171
+ type="number"
172
+ value={itemValue}
173
+ onChange={e => handleUpdateItem(index, e.target.value)}
174
+ disabled={readOnly}
175
+ min={0}
176
+ step={1}
177
+ className="flex-1"
178
+ />
179
+ </div>
180
+ );
181
+ }
182
+
183
+ // Fall back to type-based rendering
184
+ switch (argType) {
185
+ case 'boolean':
186
+ return (
187
+ <Switch
188
+ checked={itemValue === 'true'}
189
+ onCheckedChange={checked => handleUpdateItem(index, checked.toString())}
190
+ disabled={readOnly}
191
+ />
192
+ );
193
+
194
+ case 'int':
195
+ case 'float': {
196
+ const isFloat = argType === 'float';
197
+ return (
198
+ <Input
199
+ type="number"
200
+ value={itemValue}
201
+ onChange={e => handleUpdateItem(index, e.target.value)}
202
+ disabled={readOnly}
203
+ step={isFloat ? 0.01 : 1}
204
+ />
205
+ );
206
+ }
207
+ case 'datetime':
208
+ return (
209
+ <DateTimeInput
210
+ value={itemValue ? new Date(itemValue) : new Date()}
211
+ onChange={val => handleUpdateItem(index, val.toISOString())}
212
+ disabled={readOnly}
213
+ />
214
+ );
215
+
216
+ default:
217
+ return <Input type="text" {...commonProps} placeholder="Enter value..." />;
218
+ }
219
+ };
220
+
221
+ // Render new item input (similar logic but for newItemValue)
222
+ const renderNewItemInput = () => {
223
+ const argType = definition.type as ConfigArgType;
224
+ const uiComponent = (definition.ui as any)?.component;
225
+
226
+ const commonProps = {
227
+ value: newItemValue,
228
+ onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
229
+ setNewItemValue(e.target.value),
230
+ disabled: readOnly,
231
+ onKeyPress: handleKeyPress,
232
+ };
233
+
234
+ switch (uiComponent) {
235
+ case 'boolean-form-input': {
236
+ return (
237
+ <Switch
238
+ checked={newItemValue === 'true'}
239
+ onCheckedChange={checked => setNewItemValue(checked.toString())}
240
+ disabled={readOnly}
241
+ />
242
+ );
243
+ }
244
+ case 'select-form-input': {
245
+ const options = (definition.ui as any)?.options || [];
246
+ return (
247
+ <Select value={newItemValue} onValueChange={setNewItemValue} disabled={readOnly}>
248
+ <SelectTrigger>
249
+ <SelectValue placeholder="Select value..." />
250
+ </SelectTrigger>
251
+ <SelectContent>
252
+ {options.map((option: any) => (
253
+ <SelectItem key={option.value} value={option.value}>
254
+ {typeof option.label === 'string'
255
+ ? option.label
256
+ : option.label?.[0]?.value || option.value}
257
+ </SelectItem>
258
+ ))}
259
+ </SelectContent>
260
+ </Select>
261
+ );
262
+ }
263
+ case 'textarea-form-input': {
264
+ return (
265
+ <Textarea
266
+ {...commonProps}
267
+ placeholder="Enter text..."
268
+ rows={2}
269
+ className="bg-background"
270
+ />
271
+ );
272
+ }
273
+ case 'date-form-input': {
274
+ return <DateTimeInput value={newItemValue} onChange={setNewItemValue} disabled={readOnly} />;
275
+ }
276
+ case 'number-form-input': {
277
+ const ui = definition.ui as any;
278
+ const isFloat = argType === 'float';
279
+ return (
280
+ <Input
281
+ type="number"
282
+ value={newItemValue}
283
+ onChange={e => setNewItemValue(e.target.value)}
284
+ disabled={readOnly}
285
+ min={ui?.min}
286
+ max={ui?.max}
287
+ step={ui?.step || (isFloat ? 0.01 : 1)}
288
+ placeholder="Enter number..."
289
+ onKeyPress={handleKeyPress}
290
+ className="bg-background"
291
+ />
292
+ );
293
+ }
294
+ case 'currency-form-input': {
295
+ return (
296
+ <div className="flex items-center">
297
+ <span className="mr-2 text-sm text-muted-foreground">$</span>
298
+ <Input
299
+ type="number"
300
+ value={newItemValue}
301
+ onChange={e => setNewItemValue(e.target.value)}
302
+ disabled={readOnly}
303
+ min={0}
304
+ step={1}
305
+ placeholder="Enter amount..."
306
+ onKeyPress={handleKeyPress}
307
+ className="flex-1 bg-background"
308
+ />
309
+ </div>
310
+ );
311
+ }
312
+ }
313
+
314
+ // Fall back to type-based rendering
315
+ switch (argType) {
316
+ case 'boolean':
317
+ return (
318
+ <Switch
319
+ checked={newItemValue === 'true'}
320
+ onCheckedChange={checked => setNewItemValue(checked.toString())}
321
+ disabled={readOnly}
322
+ />
323
+ );
324
+ case 'int':
325
+ case 'float': {
326
+ const isFloat = argType === 'float';
327
+ return (
328
+ <Input
329
+ type="number"
330
+ value={newItemValue}
331
+ onChange={e => setNewItemValue(e.target.value)}
332
+ disabled={readOnly}
333
+ step={isFloat ? 0.01 : 1}
334
+ placeholder="Enter number..."
335
+ onKeyPress={handleKeyPress}
336
+ className="bg-background"
337
+ />
338
+ );
339
+ }
340
+ case 'datetime': {
341
+ return (
342
+ <DateTimeInput
343
+ value={newItemValue ? new Date(newItemValue) : new Date()}
344
+ onChange={val => setNewItemValue(val.toISOString())}
345
+ disabled={readOnly}
346
+ />
347
+ );
348
+ }
349
+ default: {
350
+ return (
351
+ <Input
352
+ type="text"
353
+ {...commonProps}
354
+ placeholder="Enter value..."
355
+ className="bg-background"
356
+ />
357
+ );
358
+ }
359
+ }
360
+ };
361
+
362
+ if (readOnly) {
363
+ return (
364
+ <div className="space-y-2">
365
+ {arrayValue.map((item, index) => (
366
+ <div key={index + item} className="flex items-center gap-2 p-2 bg-muted rounded-md">
367
+ <span className="flex-1">{item}</span>
368
+ </div>
369
+ ))}
370
+ {arrayValue.length === 0 && <div className="text-sm text-muted-foreground">No items</div>}
371
+ </div>
372
+ );
373
+ }
374
+
375
+ return (
376
+ <div className="space-y-2">
377
+ {/* Existing items */}
378
+ {arrayValue.map((item, index) => (
379
+ <div key={index + item} className="flex items-center gap-2">
380
+ <div className="flex-1">{renderItemInput(item, index)}</div>
381
+ <Button
382
+ variant="outline"
383
+ size="sm"
384
+ onClick={() => handleRemoveItem(index)}
385
+ disabled={readOnly}
386
+ type="button"
387
+ >
388
+ <X className="h-4 w-4" />
389
+ </Button>
390
+ </div>
391
+ ))}
392
+
393
+ {/* Add new item */}
394
+ <div className="flex items-center gap-2 p-2 border border-dashed rounded-md">
395
+ <div className="flex-1">{renderNewItemInput()}</div>
396
+ <Button
397
+ variant="outline"
398
+ size="sm"
399
+ onClick={handleAddItem}
400
+ disabled={readOnly || !newItemValue.trim()}
401
+ type="button"
402
+ >
403
+ <Plus className="h-4 w-4" />
404
+ </Button>
405
+ </div>
406
+
407
+ {arrayValue.length === 0 && (
408
+ <div className="text-sm text-muted-foreground">
409
+ No items added yet. Use the input above to add items.
410
+ </div>
411
+ )}
412
+ </div>
413
+ );
414
+ }
415
+
416
+ function parseArrayValue(value: string): string[] {
417
+ if (!value) return [];
418
+
419
+ try {
420
+ const parsed = JSON.parse(value);
421
+ return Array.isArray(parsed) ? parsed.map(String) : [String(parsed)];
422
+ } catch {
423
+ // If not JSON, try comma-separated values
424
+ return value.includes(',')
425
+ ? value
426
+ .split(',')
427
+ .map(s => s.trim())
428
+ .filter(Boolean)
429
+ : value
430
+ ? [value]
431
+ : [];
432
+ }
433
+ }