@vendure/dashboard 3.6.0-minor-202511061555 → 3.6.0-minor-202512161252
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/dist/plugin/constants.js +2 -2
- package/dist/vite/constants.js +1 -0
- package/dist/vite/utils/compiler.d.ts +1 -0
- package/dist/vite/utils/compiler.js +5 -4
- package/dist/vite/utils/get-dashboard-paths.d.ts +5 -0
- package/dist/vite/utils/get-dashboard-paths.js +20 -0
- package/dist/vite/vite-plugin-dashboard-metadata.js +2 -1
- package/dist/vite/vite-plugin-tailwind-source.js +2 -15
- package/dist/vite/vite-plugin-translations.d.ts +10 -1
- package/dist/vite/vite-plugin-translations.js +156 -45
- package/dist/vite/vite-plugin-vendure-dashboard.d.ts +12 -0
- package/dist/vite/vite-plugin-vendure-dashboard.js +1 -0
- package/lingui.config.js +1 -0
- package/package.json +7 -7
- package/src/app/routeTree.gen.ts +1221 -0
- package/src/app/routes/_authenticated/_administrators/administrators.tsx +9 -12
- package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +6 -9
- package/src/app/routes/_authenticated/_channels/channels.tsx +9 -12
- package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_collections/collections.tsx +9 -12
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_countries/countries.tsx +9 -12
- package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +9 -12
- package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_customers/components/customer-history/index.ts +0 -1
- package/src/app/routes/_authenticated/_customers/customers.tsx +9 -12
- package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_facets/facets.tsx +9 -12
- package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +10 -13
- package/src/app/routes/_authenticated/_orders/components/add-surcharge-form.tsx +139 -0
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +3 -0
- package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +3 -1
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +3 -3
- package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +41 -41
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +1 -1
- package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +49 -11
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +4 -1
- package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +2 -3
- package/src/app/routes/_authenticated/_orders/orders.tsx +3 -3
- package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +12 -3
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +27 -30
- package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +23 -0
- package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +9 -12
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_product-variants/components/add-currency-dropdown.tsx +3 -3
- package/src/app/routes/_authenticated/_product-variants/components/add-stock-location-dropdown.tsx +2 -2
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +10 -12
- package/src/app/routes/_authenticated/_products/products.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_products/products.tsx +15 -18
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_profile/profile.tsx +3 -3
- package/src/app/routes/_authenticated/_promotions/promotions.tsx +9 -12
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_roles/roles.tsx +9 -12
- package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_sellers/sellers.tsx +9 -12
- package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +11 -12
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +19 -20
- package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +9 -12
- package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_system/healthchecks.tsx +2 -3
- package/src/app/routes/_authenticated/_system/job-queue.tsx +3 -3
- package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +9 -12
- package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +9 -12
- package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_zones/components/zone-bulk-actions.tsx +49 -1
- package/src/app/routes/_authenticated/_zones/components/zone-countries-table.tsx +34 -16
- package/src/app/routes/_authenticated/_zones/zones.tsx +9 -12
- package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/index.tsx +5 -3
- package/src/i18n/locales/bg.po +3436 -0
- package/src/lib/components/data-input/datetime-input.tsx +1 -1
- package/src/lib/components/data-input/default-relation-input.tsx +1 -1
- package/src/lib/components/data-input/relation-selector.tsx +1 -1
- package/src/lib/components/data-input/string-list-input.tsx +188 -26
- package/src/lib/components/data-input/struct-form-input.tsx +175 -174
- package/src/lib/components/data-table/column-header-wrapper.tsx +1 -1
- package/src/lib/components/data-table/data-table-filter-badge.tsx +2 -2
- package/src/lib/components/data-table/data-table.tsx +1 -1
- package/src/lib/components/data-table/use-generated-columns.tsx +1 -1
- package/src/lib/components/layout/channel-switcher.tsx +6 -2
- package/src/lib/components/layout/content-language-selector.tsx +6 -7
- package/src/lib/components/layout/dev-mode-indicator.tsx +7 -3
- package/src/lib/components/layout/language-dialog.tsx +26 -13
- package/src/lib/components/layout/manage-languages-dialog.tsx +10 -29
- package/src/lib/components/layout/nav-item-wrapper.tsx +1 -1
- package/src/lib/components/shared/asset/asset-gallery.tsx +8 -3
- package/src/lib/components/shared/configurable-operation-multi-selector.tsx +14 -16
- package/src/lib/components/shared/custom-fields-form.tsx +14 -9
- package/src/lib/components/shared/language-selector.tsx +14 -6
- package/src/lib/components/shared/multi-select.tsx +1 -1
- package/src/lib/components/shared/navigation-confirmation.tsx +1 -1
- package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +4 -4
- package/src/lib/components/ui/carousel.tsx +2 -2
- package/src/lib/components/ui/chart.tsx +1 -1
- package/src/lib/components/ui/context-menu.tsx +1 -1
- package/src/lib/components/ui/drawer.tsx +1 -1
- package/src/lib/components/ui/grid-layout.tsx +1 -1
- package/src/lib/components/ui/input-group.tsx +1 -0
- package/src/lib/components/ui/input-otp.tsx +1 -1
- package/src/lib/components/ui/menubar.tsx +1 -1
- package/src/lib/components/ui/navigation-menu.tsx +1 -1
- package/src/lib/components/ui/progress.tsx +1 -1
- package/src/lib/components/ui/radio-group.tsx +1 -1
- package/src/lib/components/ui/resizable.tsx +1 -1
- package/src/lib/components/ui/select.tsx +1 -1
- package/src/lib/components/ui/slider.tsx +1 -1
- package/src/lib/components/ui/toggle-group.tsx +2 -2
- package/src/lib/components/ui/toggle.tsx +1 -1
- package/src/lib/framework/component-registry/component-registry.tsx +2 -6
- package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +907 -1
- package/src/lib/framework/document-introspection/add-custom-fields.ts +248 -119
- package/src/lib/framework/extension-api/display-component-extensions.tsx +4 -3
- package/src/lib/framework/extension-api/logic/detail-forms.ts +0 -13
- package/src/lib/framework/extension-api/logic/navigation.ts +1 -1
- package/src/lib/framework/extension-api/types/data-table.ts +4 -2
- package/src/lib/framework/extension-api/types/layout.ts +34 -1
- package/src/lib/framework/extension-api/types/navigation.ts +7 -2
- package/src/lib/framework/form-engine/use-generated-form.tsx +7 -1
- package/src/lib/framework/history-entry/history-entry.tsx +1 -1
- package/src/lib/framework/layout-engine/action-bar-item-wrapper.tsx +185 -0
- package/src/lib/framework/layout-engine/dev-mode-button.tsx +15 -13
- package/src/lib/framework/layout-engine/location-wrapper.tsx +3 -1
- package/src/lib/framework/layout-engine/page-layout.spec.tsx +138 -0
- package/src/lib/framework/layout-engine/page-layout.tsx +294 -69
- package/src/lib/framework/nav-menu/nav-menu-extensions.ts +1 -1
- package/src/lib/framework/page/detail-page-route-loader.tsx +1 -1
- package/src/lib/framework/page/page-api.ts +1 -1
- package/src/lib/framework/page/use-detail-page.ts +4 -2
- package/src/lib/framework/page/use-extended-router.tsx +20 -16
- package/src/lib/framework/registry/registry-types.ts +2 -1
- package/src/lib/graphql/api.ts +3 -8
- package/src/lib/graphql/graphql-env.d.ts +29 -10
- package/src/lib/hooks/use-permissions.ts +3 -3
- package/src/lib/hooks/use-sorted-languages.ts +41 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/lib/load-i18n-messages.ts +4 -1
- package/src/lib/providers/channel-provider.tsx +11 -7
- package/src/lib/utils/config-utils.ts +19 -0
- package/src/lib/virtual.d.ts +3 -0
- package/LICENSE.md +0 -42
- package/src/app/routes/_authenticated/_facets/components/edit-facet-value.tsx +0 -129
- /package/src/{app/routes/_authenticated/_global-settings → lib}/utils/global-languages.ts +0 -0
|
@@ -27,6 +27,7 @@ let globalCustomFieldsMap: Map<string, CustomFieldConfig[]> = new Map();
|
|
|
27
27
|
|
|
28
28
|
// Memoization cache using WeakMap to avoid memory leaks
|
|
29
29
|
const memoizationCache = new WeakMap<DocumentNode, Map<string, TypedDocumentNode<any, any>>>();
|
|
30
|
+
const fragmentMemoizationCache = new WeakMap<DocumentNode, Map<string, TypedDocumentNode<any, any>>>();
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Creates a cache key for the options object
|
|
@@ -34,6 +35,7 @@ const memoizationCache = new WeakMap<DocumentNode, Map<string, TypedDocumentNode
|
|
|
34
35
|
function createOptionsKey(options?: {
|
|
35
36
|
customFieldsMap?: Map<string, CustomFieldConfig[]>;
|
|
36
37
|
includeCustomFields?: string[];
|
|
38
|
+
includeNestedFragments?: string[];
|
|
37
39
|
}): string {
|
|
38
40
|
if (!options) return 'default';
|
|
39
41
|
|
|
@@ -51,6 +53,10 @@ function createOptionsKey(options?: {
|
|
|
51
53
|
parts.push(`include:${options.includeCustomFields.sort().join(',')}`);
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
if (options.includeNestedFragments) {
|
|
57
|
+
parts.push(`nested:${options.includeNestedFragments.sort().join(',')}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
return parts.join('|') || 'default';
|
|
55
61
|
}
|
|
56
62
|
|
|
@@ -82,10 +88,232 @@ export function getCustomFieldsMap() {
|
|
|
82
88
|
return globalCustomFieldsMap;
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
/**
|
|
92
|
+
* @description
|
|
93
|
+
* Internal helper function that applies custom fields to a selection set for a given entity type.
|
|
94
|
+
* This is the core logic extracted for reuse.
|
|
95
|
+
*/
|
|
96
|
+
function applyCustomFieldsToSelection(
|
|
97
|
+
typeName: string,
|
|
98
|
+
selectionSet: SelectionSetNode,
|
|
99
|
+
customFields: Map<string, CustomFieldConfig[]>,
|
|
100
|
+
options?: {
|
|
101
|
+
includeCustomFields?: string[];
|
|
102
|
+
},
|
|
103
|
+
): void {
|
|
104
|
+
let entityType = typeName;
|
|
105
|
+
|
|
106
|
+
if (entityType === ('OrderAddress' as any)) {
|
|
107
|
+
// OrderAddress is a special case of the Address entity, and shares its custom fields
|
|
108
|
+
// so we treat it as an alias
|
|
109
|
+
entityType = 'Address';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (entityType === ('Country' as any)) {
|
|
113
|
+
// Country is an alias of Region
|
|
114
|
+
entityType = 'Region';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const customFieldsForType = customFields.get(entityType);
|
|
118
|
+
if (customFieldsForType && customFieldsForType.length) {
|
|
119
|
+
// Check if there is already a customFields field in the fragment
|
|
120
|
+
// to avoid duplication
|
|
121
|
+
const existingCustomFieldsField = selectionSet.selections.find(
|
|
122
|
+
selection => isFieldNode(selection) && selection.name.value === 'customFields',
|
|
123
|
+
) as FieldNode | undefined;
|
|
124
|
+
const selectionNodes: SelectionNode[] = customFieldsForType
|
|
125
|
+
.filter(
|
|
126
|
+
field => !options?.includeCustomFields || options?.includeCustomFields.includes(field.name),
|
|
127
|
+
)
|
|
128
|
+
.map(
|
|
129
|
+
customField =>
|
|
130
|
+
({
|
|
131
|
+
kind: Kind.FIELD,
|
|
132
|
+
name: {
|
|
133
|
+
kind: Kind.NAME,
|
|
134
|
+
value: customField.name,
|
|
135
|
+
},
|
|
136
|
+
// For "relation" custom fields, we need to also select
|
|
137
|
+
// all the scalar fields of the related type
|
|
138
|
+
...(customField.type === 'relation'
|
|
139
|
+
? {
|
|
140
|
+
selectionSet: {
|
|
141
|
+
kind: Kind.SELECTION_SET,
|
|
142
|
+
selections: (
|
|
143
|
+
customField as RelationCustomFieldFragment
|
|
144
|
+
).scalarFields.map(f => ({
|
|
145
|
+
kind: Kind.FIELD,
|
|
146
|
+
name: { kind: Kind.NAME, value: f },
|
|
147
|
+
})),
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
: {}),
|
|
151
|
+
...(customField.type === 'struct'
|
|
152
|
+
? {
|
|
153
|
+
selectionSet: {
|
|
154
|
+
kind: Kind.SELECTION_SET,
|
|
155
|
+
selections: (customField as StructCustomFieldFragment).fields.map(
|
|
156
|
+
f => ({
|
|
157
|
+
kind: Kind.FIELD,
|
|
158
|
+
name: { kind: Kind.NAME, value: f.name },
|
|
159
|
+
}),
|
|
160
|
+
),
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
: {}),
|
|
164
|
+
}) as FieldNode,
|
|
165
|
+
);
|
|
166
|
+
if (!existingCustomFieldsField) {
|
|
167
|
+
// If no customFields field exists, add one
|
|
168
|
+
(selectionSet.selections as SelectionNode[]).push({
|
|
169
|
+
kind: Kind.FIELD,
|
|
170
|
+
name: {
|
|
171
|
+
kind: Kind.NAME,
|
|
172
|
+
value: 'customFields',
|
|
173
|
+
},
|
|
174
|
+
selectionSet: {
|
|
175
|
+
kind: Kind.SELECTION_SET,
|
|
176
|
+
selections: selectionNodes,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
// If a customFields field already exists, add the custom fields
|
|
181
|
+
// to the existing selection set
|
|
182
|
+
(existingCustomFieldsField.selectionSet as any) = {
|
|
183
|
+
kind: Kind.SELECTION_SET,
|
|
184
|
+
selections: selectionNodes,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const localizedFields = customFieldsForType.filter(
|
|
189
|
+
field => field.type === 'localeString' || field.type === 'localeText',
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const translationsField = selectionSet.selections
|
|
193
|
+
.filter(isFieldNode)
|
|
194
|
+
.find(field => field.name.value === 'translations');
|
|
195
|
+
|
|
196
|
+
if (localizedFields.length && translationsField && translationsField.selectionSet) {
|
|
197
|
+
(translationsField.selectionSet.selections as SelectionNode[]).push({
|
|
198
|
+
name: {
|
|
199
|
+
kind: Kind.NAME,
|
|
200
|
+
value: 'customFields',
|
|
201
|
+
},
|
|
202
|
+
kind: Kind.FIELD,
|
|
203
|
+
selectionSet: {
|
|
204
|
+
kind: Kind.SELECTION_SET,
|
|
205
|
+
selections: localizedFields.map(
|
|
206
|
+
customField =>
|
|
207
|
+
({
|
|
208
|
+
kind: Kind.FIELD,
|
|
209
|
+
name: {
|
|
210
|
+
kind: Kind.NAME,
|
|
211
|
+
value: customField.name,
|
|
212
|
+
},
|
|
213
|
+
}) as FieldNode,
|
|
214
|
+
),
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @description
|
|
223
|
+
* Adds custom fields to a single fragment document. This is a more granular version of `addCustomFields()`
|
|
224
|
+
* that operates on individual fragments, allowing for better composability.
|
|
225
|
+
*
|
|
226
|
+
* **Important behavior with fragment dependencies:**
|
|
227
|
+
* - When a document contains multiple fragments (e.g., a main fragment with dependencies passed to `graphql()`),
|
|
228
|
+
* only the **first fragment** is modified with custom fields.
|
|
229
|
+
* - Any additional fragments (dependencies) are left untouched in the output document.
|
|
230
|
+
* - This allows you to selectively control which fragments get custom fields.
|
|
231
|
+
*
|
|
232
|
+
* This function is memoized to return a stable identity for given inputs.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```typescript
|
|
236
|
+
* // Basic usage
|
|
237
|
+
* const modifiedFragment = addCustomFieldsToFragment(orderDetailFragment, {
|
|
238
|
+
* includeCustomFields: ['reviewCount', 'priority']
|
|
239
|
+
* });
|
|
240
|
+
*
|
|
241
|
+
* // With fragment dependencies (only OrderDetail gets custom fields, OrderLine doesn't)
|
|
242
|
+
* const orderDetailFragment = graphql(
|
|
243
|
+
* `fragment OrderDetail on Order {
|
|
244
|
+
* id
|
|
245
|
+
* lines { ...OrderLine }
|
|
246
|
+
* }`,
|
|
247
|
+
* [orderLineFragment] // This dependency won't get custom fields
|
|
248
|
+
* );
|
|
249
|
+
* const modified = addCustomFieldsToFragment(orderDetailFragment);
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
export function addCustomFieldsToFragment<T, V extends Variables = Variables>(
|
|
253
|
+
fragmentDocument: DocumentNode | TypedDocumentNode<T, V>,
|
|
254
|
+
options?: {
|
|
255
|
+
customFieldsMap?: Map<string, CustomFieldConfig[]>;
|
|
256
|
+
includeCustomFields?: string[];
|
|
257
|
+
},
|
|
258
|
+
): TypedDocumentNode<T, V> {
|
|
259
|
+
const optionsKey = createOptionsKey(options);
|
|
260
|
+
|
|
261
|
+
// Check if we have a cached result for this fragment and options
|
|
262
|
+
let documentCache = fragmentMemoizationCache.get(fragmentDocument);
|
|
263
|
+
if (!documentCache) {
|
|
264
|
+
documentCache = new Map();
|
|
265
|
+
fragmentMemoizationCache.set(fragmentDocument, documentCache);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const cachedResult = documentCache.get(optionsKey);
|
|
269
|
+
if (cachedResult) {
|
|
270
|
+
return cachedResult as TypedDocumentNode<T, V>;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Validate that this is a fragment-only document
|
|
274
|
+
const fragmentDefs = fragmentDocument.definitions.filter(isFragmentDefinition);
|
|
275
|
+
const queryDefs = fragmentDocument.definitions.filter(isOperationDefinition);
|
|
276
|
+
|
|
277
|
+
if (queryDefs.length > 0) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
'addCustomFieldsToFragment() expects a fragment-only document. Use addCustomFields() for documents with queries.',
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (fragmentDefs.length === 0) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
'addCustomFieldsToFragment() expects a document with at least one fragment definition.',
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Clone the document
|
|
290
|
+
const clone = JSON.parse(JSON.stringify(fragmentDocument)) as DocumentNode;
|
|
291
|
+
const customFields = options?.customFieldsMap || globalCustomFieldsMap;
|
|
292
|
+
|
|
293
|
+
// Only modify the first fragment (the main one)
|
|
294
|
+
// Any additional fragments are dependencies and should be left untouched
|
|
295
|
+
const fragmentDef = clone.definitions.find(isFragmentDefinition) as FragmentDefinitionNode;
|
|
296
|
+
|
|
297
|
+
// Apply custom fields only to the first/main fragment
|
|
298
|
+
applyCustomFieldsToSelection(
|
|
299
|
+
fragmentDef.typeCondition.name.value,
|
|
300
|
+
fragmentDef.selectionSet,
|
|
301
|
+
customFields,
|
|
302
|
+
options,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Cache the result before returning
|
|
306
|
+
documentCache.set(optionsKey, clone);
|
|
307
|
+
return clone;
|
|
308
|
+
}
|
|
309
|
+
|
|
85
310
|
/**
|
|
86
311
|
* Given a GraphQL AST (DocumentNode), this function looks for fragment definitions and adds and configured
|
|
87
312
|
* custom fields to those fragments.
|
|
88
313
|
*
|
|
314
|
+
* By default, only adds custom fields to top-level fragments (those used directly in the query result).
|
|
315
|
+
* Use `includeNestedFragments` to also add custom fields to specific nested fragments.
|
|
316
|
+
*
|
|
89
317
|
* This function is memoized to return a stable identity for given inputs.
|
|
90
318
|
*/
|
|
91
319
|
export function addCustomFields<T, V extends Variables = Variables>(
|
|
@@ -93,6 +321,18 @@ export function addCustomFields<T, V extends Variables = Variables>(
|
|
|
93
321
|
options?: {
|
|
94
322
|
customFieldsMap?: Map<string, CustomFieldConfig[]>;
|
|
95
323
|
includeCustomFields?: string[];
|
|
324
|
+
/**
|
|
325
|
+
* Names of nested fragments that should also get custom fields.
|
|
326
|
+
* By default, only top-level fragments get custom fields.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```typescript
|
|
330
|
+
* addCustomFields(orderDetailDocument, {
|
|
331
|
+
* includeNestedFragments: ['OrderLine', 'Asset']
|
|
332
|
+
* })
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
includeNestedFragments?: string[];
|
|
96
336
|
},
|
|
97
337
|
): TypedDocumentNode<T, V> {
|
|
98
338
|
const optionsKey = createOptionsKey(options);
|
|
@@ -168,9 +408,13 @@ export function addCustomFields<T, V extends Variables = Variables>(
|
|
|
168
408
|
|
|
169
409
|
for (const fragmentDef of fragmentDefs) {
|
|
170
410
|
if (hasQueries) {
|
|
171
|
-
// If we have queries,
|
|
172
|
-
//
|
|
173
|
-
|
|
411
|
+
// If we have queries, add custom fields to:
|
|
412
|
+
// 1. Fragments used at the top level (in the main query result)
|
|
413
|
+
// 2. Fragments explicitly listed in includeNestedFragments option
|
|
414
|
+
const isTopLevel = topLevelFragments.has(fragmentDef.name.value);
|
|
415
|
+
const isExplicitlyIncluded = options?.includeNestedFragments?.includes(fragmentDef.name.value);
|
|
416
|
+
|
|
417
|
+
if (isTopLevel || isExplicitlyIncluded) {
|
|
174
418
|
targetNodes.push({
|
|
175
419
|
typeName: fragmentDef.typeCondition.name.value,
|
|
176
420
|
selectionSet: fragmentDef.selectionSet,
|
|
@@ -187,122 +431,7 @@ export function addCustomFields<T, V extends Variables = Variables>(
|
|
|
187
431
|
}
|
|
188
432
|
|
|
189
433
|
for (const target of targetNodes) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (entityType === ('OrderAddress' as any)) {
|
|
193
|
-
// OrderAddress is a special case of the Address entity, and shares its custom fields
|
|
194
|
-
// so we treat it as an alias
|
|
195
|
-
entityType = 'Address';
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (entityType === ('Country' as any)) {
|
|
199
|
-
// Country is an alias of Region
|
|
200
|
-
entityType = 'Region';
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const customFieldsForType = customFields.get(entityType);
|
|
204
|
-
if (customFieldsForType && customFieldsForType.length) {
|
|
205
|
-
// Check if there is already a customFields field in the fragment
|
|
206
|
-
// to avoid duplication
|
|
207
|
-
const existingCustomFieldsField = target.selectionSet.selections.find(
|
|
208
|
-
selection => isFieldNode(selection) && selection.name.value === 'customFields',
|
|
209
|
-
) as FieldNode | undefined;
|
|
210
|
-
const selectionNodes: SelectionNode[] = customFieldsForType
|
|
211
|
-
.filter(
|
|
212
|
-
field =>
|
|
213
|
-
!options?.includeCustomFields || options?.includeCustomFields.includes(field.name),
|
|
214
|
-
)
|
|
215
|
-
.map(
|
|
216
|
-
customField =>
|
|
217
|
-
({
|
|
218
|
-
kind: Kind.FIELD,
|
|
219
|
-
name: {
|
|
220
|
-
kind: Kind.NAME,
|
|
221
|
-
value: customField.name,
|
|
222
|
-
},
|
|
223
|
-
// For "relation" custom fields, we need to also select
|
|
224
|
-
// all the scalar fields of the related type
|
|
225
|
-
...(customField.type === 'relation'
|
|
226
|
-
? {
|
|
227
|
-
selectionSet: {
|
|
228
|
-
kind: Kind.SELECTION_SET,
|
|
229
|
-
selections: (
|
|
230
|
-
customField as RelationCustomFieldFragment
|
|
231
|
-
).scalarFields.map(f => ({
|
|
232
|
-
kind: Kind.FIELD,
|
|
233
|
-
name: { kind: Kind.NAME, value: f },
|
|
234
|
-
})),
|
|
235
|
-
},
|
|
236
|
-
}
|
|
237
|
-
: {}),
|
|
238
|
-
...(customField.type === 'struct'
|
|
239
|
-
? {
|
|
240
|
-
selectionSet: {
|
|
241
|
-
kind: Kind.SELECTION_SET,
|
|
242
|
-
selections: (customField as StructCustomFieldFragment).fields.map(
|
|
243
|
-
f => ({
|
|
244
|
-
kind: Kind.FIELD,
|
|
245
|
-
name: { kind: Kind.NAME, value: f.name },
|
|
246
|
-
}),
|
|
247
|
-
),
|
|
248
|
-
},
|
|
249
|
-
}
|
|
250
|
-
: {}),
|
|
251
|
-
}) as FieldNode,
|
|
252
|
-
);
|
|
253
|
-
if (!existingCustomFieldsField) {
|
|
254
|
-
// If no customFields field exists, add one
|
|
255
|
-
(target.selectionSet.selections as SelectionNode[]).push({
|
|
256
|
-
kind: Kind.FIELD,
|
|
257
|
-
name: {
|
|
258
|
-
kind: Kind.NAME,
|
|
259
|
-
value: 'customFields',
|
|
260
|
-
},
|
|
261
|
-
selectionSet: {
|
|
262
|
-
kind: Kind.SELECTION_SET,
|
|
263
|
-
selections: selectionNodes,
|
|
264
|
-
},
|
|
265
|
-
});
|
|
266
|
-
} else {
|
|
267
|
-
// If a customFields field already exists, add the custom fields
|
|
268
|
-
// to the existing selection set
|
|
269
|
-
(existingCustomFieldsField.selectionSet as any) = {
|
|
270
|
-
kind: Kind.SELECTION_SET,
|
|
271
|
-
selections: selectionNodes,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const localizedFields = customFieldsForType.filter(
|
|
276
|
-
field => field.type === 'localeString' || field.type === 'localeText',
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
const translationsField = target.selectionSet.selections
|
|
280
|
-
.filter(isFieldNode)
|
|
281
|
-
.find(field => field.name.value === 'translations');
|
|
282
|
-
|
|
283
|
-
if (localizedFields.length && translationsField && translationsField.selectionSet) {
|
|
284
|
-
(translationsField.selectionSet.selections as SelectionNode[]).push({
|
|
285
|
-
name: {
|
|
286
|
-
kind: Kind.NAME,
|
|
287
|
-
value: 'customFields',
|
|
288
|
-
},
|
|
289
|
-
kind: Kind.FIELD,
|
|
290
|
-
selectionSet: {
|
|
291
|
-
kind: Kind.SELECTION_SET,
|
|
292
|
-
selections: localizedFields.map(
|
|
293
|
-
customField =>
|
|
294
|
-
({
|
|
295
|
-
kind: Kind.FIELD,
|
|
296
|
-
name: {
|
|
297
|
-
kind: Kind.NAME,
|
|
298
|
-
value: customField.name,
|
|
299
|
-
},
|
|
300
|
-
}) as FieldNode,
|
|
301
|
-
),
|
|
302
|
-
},
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
}
|
|
434
|
+
applyCustomFieldsToSelection(target.typeName, target.selectionSet, customFields, options);
|
|
306
435
|
}
|
|
307
436
|
|
|
308
437
|
// Cache the result before returning
|
|
@@ -5,6 +5,7 @@ import { Money } from '@/vdb/components/data-display/money.js';
|
|
|
5
5
|
import { VendureImage } from '@/vdb/components/shared/vendure-image.js';
|
|
6
6
|
import { DataDisplayComponent } from '../component-registry/component-registry.js';
|
|
7
7
|
import { globalRegistry } from '../registry/global-registry.js';
|
|
8
|
+
import { DataTableDisplayComponent } from './types/data-table.js';
|
|
8
9
|
|
|
9
10
|
globalRegistry.register('displayComponents', new Map<string, DataDisplayComponent>());
|
|
10
11
|
|
|
@@ -21,7 +22,7 @@ displayComponents.set('vendure:money', Money);
|
|
|
21
22
|
displayComponents.set('vendure:json', Json);
|
|
22
23
|
|
|
23
24
|
export function getDisplayComponent(id: string): DataDisplayComponent | undefined {
|
|
24
|
-
return globalRegistry.get('displayComponents').get(id);
|
|
25
|
+
return globalRegistry.get('displayComponents').get(id) as DataDisplayComponent | undefined;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -42,10 +43,10 @@ export function addDisplayComponent({
|
|
|
42
43
|
pageId: string;
|
|
43
44
|
blockId: string;
|
|
44
45
|
field: string;
|
|
45
|
-
component:
|
|
46
|
+
component: DataDisplayComponent | DataTableDisplayComponent;
|
|
46
47
|
}) {
|
|
47
48
|
const displayComponents = globalRegistry.get('displayComponents');
|
|
48
|
-
|
|
49
|
+
|
|
49
50
|
// Generate the key using the helper function
|
|
50
51
|
const key = generateDisplayComponentKey(pageId, blockId, field);
|
|
51
52
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { addDetailQueryDocument } from '@/vdb/framework/form-engine/custom-form-component-extensions.js';
|
|
2
2
|
import { parse } from 'graphql';
|
|
3
3
|
|
|
4
|
-
import { addDisplayComponent } from '../display-component-extensions.js';
|
|
5
4
|
import { addInputComponent } from '../input-component-extensions.js';
|
|
6
5
|
import { DashboardDetailFormExtensionDefinition } from '../types/detail-forms.js';
|
|
7
6
|
|
|
@@ -31,18 +30,6 @@ export function registerDetailFormExtensions(detailForms?: DashboardDetailFormEx
|
|
|
31
30
|
});
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
|
-
|
|
35
|
-
// Register display components for this detail form
|
|
36
|
-
if (detailForm.displays) {
|
|
37
|
-
for (const displayComponent of detailForm.displays) {
|
|
38
|
-
addDisplayComponent({
|
|
39
|
-
pageId: detailForm.pageId,
|
|
40
|
-
blockId: displayComponent.blockId,
|
|
41
|
-
field: displayComponent.field,
|
|
42
|
-
component: displayComponent.component,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
33
|
}
|
|
47
34
|
}
|
|
48
35
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DataDisplayComponent } from '@/vdb/framework/component-registry/component-registry.js';
|
|
2
2
|
import { Table } from '@tanstack/react-table';
|
|
3
3
|
import { CellContext } from '@tanstack/table-core';
|
|
4
4
|
import { DocumentNode } from 'graphql';
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
|
+
export type DataTableDisplayComponent = DataDisplayComponent<CellContext<any, any>>;
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* @description
|
|
9
11
|
* Allows you to define custom display components for specific columns in data tables.
|
|
@@ -24,7 +26,7 @@ export interface DashboardDataTableDisplayComponent {
|
|
|
24
26
|
* The React component that will be rendered as the display.
|
|
25
27
|
* It should accept `value` and other standard display props.
|
|
26
28
|
*/
|
|
27
|
-
component:
|
|
29
|
+
component: DataTableDisplayComponent;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export type BulkActionContext<Item extends { id: string } & Record<string, any>> = {
|
|
@@ -52,8 +52,41 @@ export interface DashboardActionBarItem {
|
|
|
52
52
|
* Any permissions that are required to display this action bar item.
|
|
53
53
|
*/
|
|
54
54
|
requiresPermission?: string | string[];
|
|
55
|
+
/**
|
|
56
|
+
* @description
|
|
57
|
+
* A unique identifier for this action bar item. This is required if you want
|
|
58
|
+
* other extensions to be able to position their items relative to this one.
|
|
59
|
+
*
|
|
60
|
+
* @since 3.5.2
|
|
61
|
+
*/
|
|
62
|
+
id?: string;
|
|
63
|
+
/**
|
|
64
|
+
* @description
|
|
65
|
+
* Position this item relative to another action bar item. The `itemId` should
|
|
66
|
+
* match the `id` of an existing action bar item (either a built-in item or one
|
|
67
|
+
* added by another extension).
|
|
68
|
+
*
|
|
69
|
+
* - `'before'`: Place this item before the target item
|
|
70
|
+
* - `'after'`: Place this item after the target item
|
|
71
|
+
* - `'replace'`: Replace the target item entirely with this item
|
|
72
|
+
*
|
|
73
|
+
* @since 3.5.2
|
|
74
|
+
*/
|
|
75
|
+
position?: ActionBarItemPosition;
|
|
55
76
|
}
|
|
56
77
|
|
|
78
|
+
/**
|
|
79
|
+
* @description
|
|
80
|
+
* The relative position of an ActionBar item. This is determined by finding an existing
|
|
81
|
+
* action bar item by its `id`, and then specifying whether your custom item should come
|
|
82
|
+
* before, after, or completely replace that item.
|
|
83
|
+
*
|
|
84
|
+
* @docsCategory extensions-api
|
|
85
|
+
* @docsPage ActionBar
|
|
86
|
+
* @since 3.5.2
|
|
87
|
+
*/
|
|
88
|
+
export type ActionBarItemPosition = { itemId: string; order: 'before' | 'after' | 'replace' };
|
|
89
|
+
|
|
57
90
|
/**
|
|
58
91
|
* @description
|
|
59
92
|
* The relative position of a PageBlock. This is determined by finding an existing
|
|
@@ -79,7 +112,7 @@ export type PageBlockPosition = { blockId: string; order: 'before' | 'after' | '
|
|
|
79
112
|
export type PageBlockLocation = {
|
|
80
113
|
pageId: string;
|
|
81
114
|
position: PageBlockPosition;
|
|
82
|
-
column: 'main' | 'side';
|
|
115
|
+
column: 'main' | 'side' | 'full';
|
|
83
116
|
};
|
|
84
117
|
|
|
85
118
|
/**
|
|
@@ -39,7 +39,7 @@ export interface DashboardRouteDefinition {
|
|
|
39
39
|
* The value is a Tanstack Router
|
|
40
40
|
* [loader function](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#route-loaders)
|
|
41
41
|
*/
|
|
42
|
-
loader?: RouteOptions['loader'];
|
|
42
|
+
loader?: RouteOptions<any>['loader'];
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* @description
|
|
@@ -47,7 +47,7 @@ export interface DashboardRouteDefinition {
|
|
|
47
47
|
* The value is a Tanstack Router
|
|
48
48
|
* [validateSearch function](https://tanstack.com/router/latest/docs/framework/react/guide/search-params#search-param-validation)
|
|
49
49
|
*/
|
|
50
|
-
validateSearch?: RouteOptions['validateSearch'];
|
|
50
|
+
validateSearch?: RouteOptions<any>['validateSearch'];
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* @description
|
|
@@ -97,4 +97,9 @@ export interface DashboardNavSectionDefinition {
|
|
|
97
97
|
* Optional order number to control the position of this section in the sidebar.
|
|
98
98
|
*/
|
|
99
99
|
order?: number;
|
|
100
|
+
/**
|
|
101
|
+
* @description
|
|
102
|
+
* Optional placement to control the position of this section in the sidebar.
|
|
103
|
+
*/
|
|
104
|
+
placement?: 'top' | 'bottom';
|
|
100
105
|
}
|
|
@@ -9,6 +9,10 @@ import { getOperationVariablesFields } from '../document-introspection/get-docum
|
|
|
9
9
|
import { createFormSchemaFromFields, getDefaultValuesFromFields } from './form-schema-tools.js';
|
|
10
10
|
import { removeEmptyIdFields, transformRelationFields } from './utils.js';
|
|
11
11
|
|
|
12
|
+
export type WithLooseCustomFields<T> = T extends { customFields?: any }
|
|
13
|
+
? Omit<T, 'customFields'> & { customFields?: T['customFields'] | unknown }
|
|
14
|
+
: T;
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* @description
|
|
14
18
|
* Options for the useGeneratedForm hook.
|
|
@@ -40,7 +44,9 @@ export interface GeneratedFormOptions<
|
|
|
40
44
|
customFieldConfig?: any[]; // Add custom field config for validation
|
|
41
45
|
setValues: (
|
|
42
46
|
entity: NonNullable<E>,
|
|
43
|
-
) =>
|
|
47
|
+
) => WithLooseCustomFields<
|
|
48
|
+
VarName extends keyof VariablesOf<T> ? VariablesOf<T>[VarName] : VariablesOf<T>
|
|
49
|
+
>;
|
|
44
50
|
onSubmit?: (
|
|
45
51
|
values: VarName extends keyof VariablesOf<T> ? VariablesOf<T>[VarName] : VariablesOf<T>,
|
|
46
52
|
) => void;
|
|
@@ -34,7 +34,7 @@ export interface HistoryEntryProps {
|
|
|
34
34
|
*
|
|
35
35
|
* ```ts
|
|
36
36
|
* const success = 'bg-success text-success-foreground';
|
|
37
|
-
* const destructive = 'bg-
|
|
37
|
+
* const destructive = 'bg-destructive text-destructive-foreground';
|
|
38
38
|
* ```
|
|
39
39
|
*/
|
|
40
40
|
timelineIconClassName?: string;
|