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