@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,260 @@
1
+ import { Button } from '@/vdb/components/ui/button.js';
2
+ import {
3
+ DropdownMenu,
4
+ DropdownMenuContent,
5
+ DropdownMenuItem,
6
+ DropdownMenuTrigger,
7
+ } from '@/vdb/components/ui/dropdown-menu.js';
8
+ import { InputComponent } from '@/vdb/framework/component-registry/dynamic-component.js';
9
+ import { api } from '@/vdb/graphql/api.js';
10
+ import { ConfigurableOperationDefFragment } from '@/vdb/graphql/fragments.js';
11
+ import { Trans } from '@/vdb/lib/trans.js';
12
+ import { DefinedInitialDataOptions, useQuery, UseQueryOptions } from '@tanstack/react-query';
13
+ import { ConfigurableOperationInput as ConfigurableOperationInputType } from '@vendure/common/lib/generated-types';
14
+ import { Plus } from 'lucide-react';
15
+ import { ConfigurableOperationInput } from './configurable-operation-input.js';
16
+
17
+ /**
18
+ * Props interface for ConfigurableOperationMultiSelector component
19
+ */
20
+ export interface ConfigurableOperationMultiSelectorProps {
21
+ /** Array of currently selected configurable operations */
22
+ value: ConfigurableOperationInputType[];
23
+ /** Callback function called when the selection changes */
24
+ onChange: (value: ConfigurableOperationInputType[]) => void;
25
+ /** GraphQL document for querying available operations (alternative to queryOptions) */
26
+ queryDocument?: any;
27
+ /** Pre-configured query options for more complex queries (alternative to queryDocument) */
28
+ queryOptions?: UseQueryOptions<any> | DefinedInitialDataOptions<any>;
29
+ /** Unique key for the query cache */
30
+ queryKey: string;
31
+ /** Dot-separated path to extract operations from query result (e.g., "promotionConditions") */
32
+ dataPath: string;
33
+ /** Text to display on the add button */
34
+ buttonText: string;
35
+ /** Title to show at the top of the dropdown menu (only when showEnhancedDropdown is true) */
36
+ dropdownTitle?: string;
37
+ /** Text to display when no operations are available (defaults to "No options found") */
38
+ emptyText?: string;
39
+ /**
40
+ * Controls the dropdown display style:
41
+ * - true: Enhanced dropdown with larger width (w-80), section title, operation descriptions + codes
42
+ * - false: Simple dropdown with standard width (w-96), just operation descriptions
43
+ *
44
+ * Enhanced style is used by promotion conditions/actions for better UX with complex operations.
45
+ * Simple style is used by collection filters for a cleaner, more compact appearance.
46
+ */
47
+ showEnhancedDropdown?: boolean;
48
+ }
49
+
50
+ type QueryData = {
51
+ [key: string]: ConfigurableOperationDefFragment[];
52
+ };
53
+
54
+ /**
55
+ * ConfigurableOperationMultiSelector - A reusable component for selecting multiple configurable operations
56
+ *
57
+ * This component provides a standardized interface for selecting multiple configurable operations such as:
58
+ * - Collection filters
59
+ * - Promotion conditions
60
+ * - Promotion actions
61
+ *
62
+ * Features:
63
+ * - Displays all selected operations with their configuration forms
64
+ * - Provides a dropdown to add new operations from available options
65
+ * - Handles individual operation updates and removals
66
+ * - Supports position-based combination mode for operations
67
+ * - Flexible query patterns (direct document or pre-configured options)
68
+ * - Two dropdown styles: enhanced (with operation codes) or simple
69
+ *
70
+ * @example
71
+ * ```tsx
72
+ * // Enhanced dropdown style (promotions)
73
+ * <ConfigurableOperationMultiSelector
74
+ * value={conditions}
75
+ * onChange={setConditions}
76
+ * queryDocument={promotionConditionsDocument}
77
+ * queryKey="promotionConditions"
78
+ * dataPath="promotionConditions"
79
+ * buttonText="Add condition"
80
+ * dropdownTitle="Available Conditions"
81
+ * showEnhancedDropdown={true}
82
+ * />
83
+ *
84
+ * // Simple dropdown style (collections)
85
+ * <ConfigurableOperationMultiSelector
86
+ * value={filters}
87
+ * onChange={setFilters}
88
+ * queryOptions={getCollectionFiltersQueryOptions}
89
+ * queryKey="getCollectionFilters"
90
+ * dataPath="collectionFilters"
91
+ * buttonText="Add collection filter"
92
+ * showEnhancedDropdown={false}
93
+ * />
94
+ * ```
95
+ */
96
+ export function ConfigurableOperationMultiSelector({
97
+ value,
98
+ onChange,
99
+ queryDocument,
100
+ queryOptions,
101
+ queryKey,
102
+ dataPath,
103
+ buttonText,
104
+ dropdownTitle,
105
+ emptyText = 'No options found',
106
+ showEnhancedDropdown = true,
107
+ }: Readonly<ConfigurableOperationMultiSelectorProps>) {
108
+ const { data } = useQuery<QueryData>(
109
+ queryOptions || {
110
+ queryKey: [queryKey],
111
+ queryFn: () => api.query(queryDocument),
112
+ staleTime: 1000 * 60 * 60 * 5,
113
+ },
114
+ );
115
+
116
+ // Extract operations from the data using the provided path
117
+ const operations = dataPath.split('.').reduce<any>((obj, key) => {
118
+ if (obj && typeof obj === 'object') {
119
+ return obj[key];
120
+ }
121
+ return undefined;
122
+ }, data) as ConfigurableOperationDefFragment[] | undefined;
123
+
124
+ const onOperationSelected = (operation: ConfigurableOperationDefFragment) => {
125
+ const operationDef = operations?.find(
126
+ (op: ConfigurableOperationDefFragment) => op.code === operation.code,
127
+ );
128
+ if (!operationDef) {
129
+ return;
130
+ }
131
+ onChange([
132
+ ...value,
133
+ {
134
+ code: operation.code,
135
+ arguments: operationDef.args.map(arg => ({
136
+ name: arg.name,
137
+ value: arg.defaultValue != null ? arg.defaultValue.toString() : '',
138
+ })),
139
+ },
140
+ ]);
141
+ };
142
+
143
+ const onOperationValueChange = (
144
+ operation: ConfigurableOperationInputType,
145
+ newVal: ConfigurableOperationInputType,
146
+ ) => {
147
+ onChange(value.map(op => (op.code === operation.code ? newVal : op)));
148
+ };
149
+
150
+ const onOperationRemove = (index: number) => {
151
+ onChange(value.filter((_, i) => i !== index));
152
+ };
153
+
154
+ const onCombinationModeChange = (index: number, newValue: boolean | string) => {
155
+ const updatedValue = [...value];
156
+ const operation = updatedValue[index];
157
+ if (operation) {
158
+ const updatedOperation = {
159
+ ...operation,
160
+ arguments: operation.arguments.map(arg =>
161
+ arg.name === 'combineWithAnd' ? { ...arg, value: newValue.toString() } : arg,
162
+ ),
163
+ };
164
+ updatedValue[index] = updatedOperation;
165
+ onChange(updatedValue);
166
+ }
167
+ };
168
+
169
+ const hasOperations = value && value.length > 0;
170
+
171
+ return (
172
+ <div className="space-y-4">
173
+ {hasOperations && (
174
+ <div className="space-y-0">
175
+ {value.map((operation, index) => {
176
+ const operationDef = operations?.find(
177
+ (op: ConfigurableOperationDefFragment) => op.code === operation.code,
178
+ );
179
+ if (!operationDef) {
180
+ return null;
181
+ }
182
+ const hasCombinationMode = operation.arguments.find(arg => arg.name === 'combineWithAnd');
183
+ return (
184
+ <div key={index + operation.code}>
185
+ {index > 0 && hasCombinationMode ? (
186
+ <div className="my-2">
187
+ <InputComponent
188
+ id="vendure:combinationModeInput"
189
+ value={
190
+ operation.arguments.find(arg => arg.name === 'combineWithAnd')
191
+ ?.value ?? 'true'
192
+ }
193
+ onChange={(newValue: boolean | string) =>
194
+ onCombinationModeChange(index, newValue)
195
+ }
196
+ position={index}
197
+ />
198
+ </div>
199
+ ) : (
200
+ <div className="h-4" />
201
+ )}
202
+ <ConfigurableOperationInput
203
+ operationDefinition={operationDef}
204
+ value={operation}
205
+ onChange={value => onOperationValueChange(operation, value)}
206
+ onRemove={() => onOperationRemove(index)}
207
+ position={index}
208
+ />
209
+ </div>
210
+ );
211
+ })}
212
+ </div>
213
+ )}
214
+
215
+ <div className={hasOperations ? 'pt-2' : ''}>
216
+ <DropdownMenu>
217
+ <DropdownMenuTrigger asChild>
218
+ <Button variant="outline" className="w-full sm:w-auto">
219
+ <Plus className="h-4 w-4" />
220
+ <Trans>{buttonText}</Trans>
221
+ </Button>
222
+ </DropdownMenuTrigger>
223
+ <DropdownMenuContent className={showEnhancedDropdown ? 'w-80' : 'w-96'} align="start">
224
+ {showEnhancedDropdown && dropdownTitle && (
225
+ <div className="px-2 py-1.5 text-sm font-medium text-muted-foreground">
226
+ {dropdownTitle}
227
+ </div>
228
+ )}
229
+ {operations?.length ? (
230
+ operations.map((operation: ConfigurableOperationDefFragment) => (
231
+ <DropdownMenuItem
232
+ key={operation.code}
233
+ onClick={() => onOperationSelected(operation)}
234
+ className={
235
+ showEnhancedDropdown
236
+ ? 'flex flex-col items-start py-3 cursor-pointer'
237
+ : undefined
238
+ }
239
+ >
240
+ {showEnhancedDropdown ? (
241
+ <>
242
+ <div className="font-medium text-sm">{operation.description}</div>
243
+ <div className="text-xs text-muted-foreground font-mono mt-1">
244
+ {operation.code}
245
+ </div>
246
+ </>
247
+ ) : (
248
+ operation.description
249
+ )}
250
+ </DropdownMenuItem>
251
+ ))
252
+ ) : (
253
+ <DropdownMenuItem>{emptyText}</DropdownMenuItem>
254
+ )}
255
+ </DropdownMenuContent>
256
+ </DropdownMenu>
257
+ </div>
258
+ </div>
259
+ );
260
+ }
@@ -0,0 +1,156 @@
1
+ import { Button } from '@/vdb/components/ui/button.js';
2
+ import {
3
+ DropdownMenu,
4
+ DropdownMenuContent,
5
+ DropdownMenuItem,
6
+ DropdownMenuTrigger,
7
+ } from '@/vdb/components/ui/dropdown-menu.js';
8
+ import { api } from '@/vdb/graphql/api.js';
9
+ import { ConfigurableOperationDefFragment } from '@/vdb/graphql/fragments.js';
10
+ import { Trans } from '@/vdb/lib/trans.js';
11
+ import { useQuery } from '@tanstack/react-query';
12
+ import { ConfigurableOperationInput as ConfigurableOperationInputType } from '@vendure/common/lib/generated-types';
13
+ import { Plus } from 'lucide-react';
14
+ import { ConfigurableOperationInput } from './configurable-operation-input.js';
15
+
16
+ /**
17
+ * Props interface for ConfigurableOperationSelector component
18
+ */
19
+ export interface ConfigurableOperationSelectorProps {
20
+ /** Current selected configurable operation value */
21
+ value: ConfigurableOperationInputType | undefined;
22
+ /** Callback function called when the selection changes */
23
+ onChange: (value: ConfigurableOperationInputType | undefined) => void;
24
+ /** GraphQL document for querying available operations */
25
+ queryDocument: any;
26
+ /** Unique key for the query cache */
27
+ queryKey: string;
28
+ /** Dot-separated path to extract operations from query result (e.g., "paymentMethodHandlers") */
29
+ dataPath: string;
30
+ /** Text to display on the selection button */
31
+ buttonText: string;
32
+ /** Text to display when no operations are available (defaults to "No options found") */
33
+ emptyText?: string;
34
+ }
35
+
36
+ type QueryData = {
37
+ [key: string]: {
38
+ [key: string]: ConfigurableOperationDefFragment[];
39
+ };
40
+ };
41
+
42
+ /**
43
+ * ConfigurableOperationSelector - A reusable component for selecting a single configurable operation
44
+ *
45
+ * This component provides a standardized interface for selecting configurable operations such as:
46
+ * - Payment method handlers
47
+ * - Payment eligibility checkers
48
+ * - Shipping calculators
49
+ * - Shipping eligibility checkers
50
+ *
51
+ * Features:
52
+ * - Displays the selected operation with its configuration form
53
+ * - Provides a dropdown to select from available operations
54
+ * - Handles operation selection with default argument values
55
+ * - Supports removal of selected operations
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * <ConfigurableOperationSelector
60
+ * value={selectedHandler}
61
+ * onChange={setSelectedHandler}
62
+ * queryDocument={paymentHandlersDocument}
63
+ * queryKey="paymentMethodHandlers"
64
+ * dataPath="paymentMethodHandlers"
65
+ * buttonText="Select Payment Handler"
66
+ * />
67
+ * ```
68
+ */
69
+ export function ConfigurableOperationSelector({
70
+ value,
71
+ onChange,
72
+ queryDocument,
73
+ queryKey,
74
+ dataPath,
75
+ buttonText,
76
+ emptyText = 'No options found',
77
+ }: Readonly<ConfigurableOperationSelectorProps>) {
78
+ const { data } = useQuery<QueryData>({
79
+ queryKey: [queryKey],
80
+ queryFn: () => api.query(queryDocument),
81
+ staleTime: 1000 * 60 * 60 * 5,
82
+ });
83
+
84
+ // Extract operations from the data using the provided path
85
+ const operations = dataPath.split('.').reduce<any>((obj, key) => {
86
+ if (obj && typeof obj === 'object') {
87
+ return obj[key];
88
+ }
89
+ return undefined;
90
+ }, data) as ConfigurableOperationDefFragment[] | undefined;
91
+
92
+ const onOperationSelected = (operation: ConfigurableOperationDefFragment) => {
93
+ const operationDef = operations?.find(
94
+ (op: ConfigurableOperationDefFragment) => op.code === operation.code,
95
+ );
96
+ if (!operationDef) {
97
+ return;
98
+ }
99
+ onChange({
100
+ code: operation.code,
101
+ arguments: operationDef.args.map(arg => ({
102
+ name: arg.name,
103
+ value: arg.defaultValue != null ? arg.defaultValue.toString() : '',
104
+ })),
105
+ });
106
+ };
107
+
108
+ const onOperationValueChange = (newVal: ConfigurableOperationInputType) => {
109
+ onChange(newVal);
110
+ };
111
+
112
+ const onOperationRemove = () => {
113
+ onChange(undefined);
114
+ };
115
+
116
+ const operationDef = operations?.find((op: ConfigurableOperationDefFragment) => op.code === value?.code);
117
+
118
+ return (
119
+ <div className="flex flex-col gap-2 mt-4">
120
+ {value && operationDef && (
121
+ <div className="flex flex-col gap-2">
122
+ <ConfigurableOperationInput
123
+ operationDefinition={operationDef}
124
+ value={value}
125
+ onChange={value => onOperationValueChange(value)}
126
+ onRemove={() => onOperationRemove()}
127
+ />
128
+ </div>
129
+ )}
130
+ <DropdownMenu>
131
+ {!value?.code && (
132
+ <DropdownMenuTrigger asChild>
133
+ <Button variant="outline" className="w-fit">
134
+ <Plus />
135
+ <Trans>{buttonText}</Trans>
136
+ </Button>
137
+ </DropdownMenuTrigger>
138
+ )}
139
+ <DropdownMenuContent className="w-96">
140
+ {operations?.length ? (
141
+ operations.map((operation: ConfigurableOperationDefFragment) => (
142
+ <DropdownMenuItem
143
+ key={operation.code}
144
+ onClick={() => onOperationSelected(operation)}
145
+ >
146
+ {operation.description}
147
+ </DropdownMenuItem>
148
+ ))
149
+ ) : (
150
+ <DropdownMenuItem>{emptyText}</DropdownMenuItem>
151
+ )}
152
+ </DropdownMenuContent>
153
+ </DropdownMenu>
154
+ </div>
155
+ );
156
+ }