@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.
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_collections/components/collection-contents-preview-table.tsx +1 -1
- package/src/app/routes/_authenticated/_collections/components/collection-filters-selector.tsx +11 -78
- package/src/app/routes/_authenticated/_payment-methods/components/payment-eligibility-checker-selector.tsx +11 -81
- package/src/app/routes/_authenticated/_payment-methods/components/payment-handler-selector.tsx +10 -77
- package/src/app/routes/_authenticated/_promotions/components/promotion-actions-selector.tsx +12 -87
- package/src/app/routes/_authenticated/_promotions/components/promotion-conditions-selector.tsx +12 -87
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-calculator-selector.tsx +10 -80
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-eligibility-checker-selector.tsx +10 -79
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +8 -6
- package/src/lib/components/data-input/combination-mode-input.tsx +52 -0
- package/src/lib/components/data-input/configurable-operation-list-input.tsx +433 -0
- package/src/lib/components/data-input/custom-field-list-input.tsx +297 -0
- package/src/lib/components/data-input/datetime-input.tsx +5 -2
- package/src/lib/components/data-input/default-relation-input.tsx +599 -0
- package/src/lib/components/data-input/index.ts +6 -0
- package/src/lib/components/data-input/product-multi-selector.tsx +426 -0
- package/src/lib/components/data-input/relation-selector.tsx +7 -6
- package/src/lib/components/data-input/select-with-options.tsx +84 -0
- package/src/lib/components/data-input/struct-form-input.tsx +324 -0
- package/src/lib/components/shared/configurable-operation-arg-input.tsx +365 -21
- package/src/lib/components/shared/configurable-operation-input.tsx +81 -41
- package/src/lib/components/shared/configurable-operation-multi-selector.tsx +260 -0
- package/src/lib/components/shared/configurable-operation-selector.tsx +156 -0
- package/src/lib/components/shared/custom-fields-form.tsx +207 -36
- package/src/lib/components/shared/multi-select.tsx +1 -1
- package/src/lib/components/ui/form.tsx +4 -4
- package/src/lib/framework/extension-api/input-component-extensions.tsx +5 -1
- package/src/lib/framework/form-engine/form-schema-tools.spec.ts +472 -0
- package/src/lib/framework/form-engine/form-schema-tools.ts +340 -5
- package/src/lib/framework/form-engine/use-generated-form.tsx +24 -8
- package/src/lib/framework/form-engine/utils.ts +3 -9
- package/src/lib/framework/layout-engine/page-layout.tsx +11 -3
- package/src/lib/framework/page/use-detail-page.ts +3 -3
- 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
|
+
}
|