@vendure/dashboard 3.3.8-master-202507290247 → 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
@@ -0,0 +1,324 @@
1
+ import { Button } from '@/vdb/components/ui/button.js';
2
+ import {
3
+ FormControl,
4
+ FormDescription,
5
+ FormField,
6
+ FormItem,
7
+ FormLabel,
8
+ FormMessage,
9
+ } from '@/vdb/components/ui/form.js';
10
+ import { Input } from '@/vdb/components/ui/input.js';
11
+ import { Switch } from '@/vdb/components/ui/switch.js';
12
+ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
13
+ import { structCustomFieldFragment } from '@/vdb/providers/server-config.js';
14
+ import { ResultOf } from 'gql.tada';
15
+ import { CheckIcon, PencilIcon, X } from 'lucide-react';
16
+ import React, { useMemo, useState } from 'react';
17
+ import { Control, ControllerRenderProps, useWatch } from 'react-hook-form';
18
+
19
+ // Import the form input component we already have
20
+ import { CustomFieldListInput } from './custom-field-list-input.js';
21
+ import { DateTimeInput } from './datetime-input.js';
22
+ import { SelectWithOptions } from './select-with-options.js';
23
+
24
+ // Use the generated types from GraphQL fragments
25
+ type StructCustomFieldConfig = ResultOf<typeof structCustomFieldFragment>;
26
+ type StructField = StructCustomFieldConfig['fields'][number];
27
+
28
+ interface StructFormInputProps {
29
+ field: ControllerRenderProps<any, any>;
30
+ fieldDef: StructCustomFieldConfig;
31
+ control: Control<any, any>;
32
+ getTranslation: (
33
+ input: Array<{ languageCode: string; value: string }> | null | undefined,
34
+ ) => string | undefined;
35
+ }
36
+
37
+ interface DisplayModeProps {
38
+ fieldDef: StructCustomFieldConfig;
39
+ watchedStructValue: Record<string, any>;
40
+ isReadonly: boolean;
41
+ setIsEditing: (value: boolean) => void;
42
+ getTranslation: (
43
+ input: Array<{ languageCode: string; value: string }> | null | undefined,
44
+ ) => string | undefined;
45
+ formatFieldValue: (value: any, structField: StructField) => React.ReactNode;
46
+ }
47
+
48
+ function DisplayMode({
49
+ fieldDef,
50
+ watchedStructValue,
51
+ isReadonly,
52
+ setIsEditing,
53
+ getTranslation,
54
+ formatFieldValue,
55
+ }: Readonly<DisplayModeProps>) {
56
+ return (
57
+ <div className="border rounded-md p-4">
58
+ <div className="flex justify-end">
59
+ {!isReadonly && (
60
+ <Button
61
+ variant="ghost"
62
+ size="sm"
63
+ onClick={() => setIsEditing(true)}
64
+ className="h-8 w-8 p-0 -mt-2 -mr-2 text-muted-foreground hover:text-foreground"
65
+ >
66
+ <PencilIcon className="h-4 w-4" />
67
+ <span className="sr-only">Edit</span>
68
+ </Button>
69
+ )}
70
+ </div>
71
+ <dl className="grid grid-cols-2 divide-y divide-muted -mt-2">
72
+ {fieldDef.fields.map(structField => (
73
+ <React.Fragment key={structField.name}>
74
+ <dt className="text-sm font-medium text-muted-foreground py-2">
75
+ {getTranslation(structField.label) ?? structField.name}
76
+ </dt>
77
+ <dd className="text-sm text-foreground py-2">
78
+ {formatFieldValue(watchedStructValue[structField.name], structField)}
79
+ </dd>
80
+ </React.Fragment>
81
+ ))}
82
+ </dl>
83
+ </div>
84
+ );
85
+ }
86
+
87
+ export function StructFormInput({ field, fieldDef, control, getTranslation }: StructFormInputProps) {
88
+ const { formatDate } = useLocalFormat();
89
+ const isReadonly = fieldDef.readonly ?? false;
90
+ const [isEditing, setIsEditing] = useState(false);
91
+
92
+ // Watch the struct field for changes to update display mode
93
+ const watchedStructValue =
94
+ useWatch({
95
+ control,
96
+ name: field.name,
97
+ defaultValue: field.value || {},
98
+ }) || {};
99
+
100
+ // Helper function to format field value for display
101
+ const formatFieldValue = (value: any, structField: StructField) => {
102
+ if (value == null) return '-';
103
+ if (structField.list) {
104
+ if (Array.isArray(value)) {
105
+ return value.length ? value.join(', ') : '-';
106
+ }
107
+ return '-';
108
+ }
109
+ switch (structField.type) {
110
+ case 'boolean':
111
+ return (
112
+ <span className={`inline-flex items-center ${value ? 'text-green-600' : 'text-red-500'}`}>
113
+ {value ? <CheckIcon className="h-4 w-4" /> : <X className="h-4 w-4" />}
114
+ </span>
115
+ );
116
+ case 'datetime':
117
+ return value ? formatDate(value, { dateStyle: 'short', timeStyle: 'short' }) : '-';
118
+ default:
119
+ return value.toString();
120
+ }
121
+ };
122
+
123
+ // Helper function to render individual struct field inputs
124
+ const renderStructFieldInput = (
125
+ structField: StructField,
126
+ inputField: ControllerRenderProps<any, any>,
127
+ ) => {
128
+ const isList = structField.list ?? false;
129
+
130
+ // Helper function to render single input for a struct field
131
+ const renderSingleStructInput = (singleField: ControllerRenderProps<any, any>) => {
132
+ switch (structField.type) {
133
+ case 'string': {
134
+ // Check if the field has options (dropdown)
135
+ const stringField = structField as any; // GraphQL union types need casting
136
+ if (stringField.options && stringField.options.length > 0) {
137
+ return (
138
+ <SelectWithOptions
139
+ field={singleField}
140
+ options={stringField.options}
141
+ disabled={isReadonly}
142
+ isListField={false}
143
+ />
144
+ );
145
+ }
146
+ return (
147
+ <Input
148
+ value={singleField.value ?? ''}
149
+ onChange={e => singleField.onChange(e.target.value)}
150
+ onBlur={singleField.onBlur}
151
+ name={singleField.name}
152
+ disabled={isReadonly}
153
+ />
154
+ );
155
+ }
156
+ case 'int':
157
+ case 'float': {
158
+ const isFloat = structField.type === 'float';
159
+ const numericField = structField as any; // GraphQL union types need casting
160
+ const min = isFloat ? numericField.floatMin : numericField.intMin;
161
+ const max = isFloat ? numericField.floatMax : numericField.intMax;
162
+ const step = isFloat ? numericField.floatStep : numericField.intStep;
163
+
164
+ return (
165
+ <Input
166
+ type="number"
167
+ value={singleField.value ?? ''}
168
+ onChange={e => {
169
+ const value = e.target.valueAsNumber;
170
+ singleField.onChange(isNaN(value) ? undefined : value);
171
+ }}
172
+ onBlur={singleField.onBlur}
173
+ name={singleField.name}
174
+ disabled={isReadonly}
175
+ min={min}
176
+ max={max}
177
+ step={step}
178
+ />
179
+ );
180
+ }
181
+ case 'boolean':
182
+ return (
183
+ <Switch
184
+ checked={singleField.value}
185
+ onCheckedChange={singleField.onChange}
186
+ disabled={isReadonly}
187
+ />
188
+ );
189
+ case 'datetime':
190
+ return (
191
+ <DateTimeInput
192
+ value={singleField.value}
193
+ onChange={singleField.onChange}
194
+ disabled={isReadonly}
195
+ />
196
+ );
197
+ default:
198
+ return (
199
+ <Input
200
+ value={singleField.value ?? ''}
201
+ onChange={e => singleField.onChange(e.target.value)}
202
+ onBlur={singleField.onBlur}
203
+ name={singleField.name}
204
+ disabled={isReadonly}
205
+ />
206
+ );
207
+ }
208
+ };
209
+
210
+ // Handle string fields with options (dropdown) - already handles list case with multi-select
211
+ if (structField.type === 'string') {
212
+ const stringField = structField as any; // GraphQL union types need casting
213
+ if (stringField.options && stringField.options.length > 0) {
214
+ return (
215
+ <SelectWithOptions
216
+ field={inputField}
217
+ options={stringField.options}
218
+ disabled={isReadonly}
219
+ isListField={isList}
220
+ />
221
+ );
222
+ }
223
+ }
224
+
225
+ // For list struct fields, wrap with list input
226
+ if (isList) {
227
+ const getDefaultValue = () => {
228
+ switch (structField.type) {
229
+ case 'string':
230
+ return '';
231
+ case 'int':
232
+ case 'float':
233
+ return 0;
234
+ case 'boolean':
235
+ return false;
236
+ case 'datetime':
237
+ return '';
238
+ default:
239
+ return '';
240
+ }
241
+ };
242
+
243
+ // Determine if the field type needs full width
244
+ const needsFullWidth = structField.type === 'text' || structField.type === 'localeText';
245
+
246
+ return (
247
+ <CustomFieldListInput
248
+ field={inputField}
249
+ disabled={isReadonly}
250
+ renderInput={(index, listItemField) => renderSingleStructInput(listItemField)}
251
+ defaultValue={getDefaultValue()}
252
+ isFullWidth={needsFullWidth}
253
+ />
254
+ );
255
+ }
256
+
257
+ // For non-list fields, render directly
258
+ return renderSingleStructInput(inputField);
259
+ };
260
+
261
+ // Edit mode - memoized to prevent focus loss from re-renders
262
+ const EditMode = useMemo(
263
+ () => (
264
+ <div className="space-y-4 border rounded-md p-4">
265
+ {!isReadonly && (
266
+ <div className="flex justify-end">
267
+ <Button
268
+ variant="ghost"
269
+ size="sm"
270
+ onClick={() => setIsEditing(false)}
271
+ className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
272
+ >
273
+ <CheckIcon className="h-4 w-4" />
274
+ <span className="sr-only">Done</span>
275
+ </Button>
276
+ </div>
277
+ )}
278
+ {fieldDef.fields.map(structField => (
279
+ <FormField
280
+ key={structField.name}
281
+ control={control}
282
+ name={`${field.name}.${structField.name}`}
283
+ render={({ field: structInputField }) => (
284
+ <FormItem>
285
+ <div className="flex items-baseline gap-4">
286
+ <div className="flex-1">
287
+ <FormLabel>
288
+ {getTranslation(structField.label) ?? structField.name}
289
+ </FormLabel>
290
+ {getTranslation(structField.description) && (
291
+ <FormDescription>
292
+ {getTranslation(structField.description)}
293
+ </FormDescription>
294
+ )}
295
+ </div>
296
+ <div className="flex-[2]">
297
+ <FormControl>
298
+ {renderStructFieldInput(structField, structInputField)}
299
+ </FormControl>
300
+ <FormMessage />
301
+ </div>
302
+ </div>
303
+ </FormItem>
304
+ )}
305
+ />
306
+ ))}
307
+ </div>
308
+ ),
309
+ [fieldDef, control, field.name, getTranslation, renderStructFieldInput, isReadonly],
310
+ );
311
+
312
+ return isEditing ? (
313
+ EditMode
314
+ ) : (
315
+ <DisplayMode
316
+ fieldDef={fieldDef}
317
+ watchedStructValue={watchedStructValue}
318
+ isReadonly={isReadonly}
319
+ setIsEditing={setIsEditing}
320
+ getTranslation={getTranslation}
321
+ formatFieldValue={formatFieldValue}
322
+ />
323
+ );
324
+ }