@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.
Files changed (152) hide show
  1. package/dist/plugin/constants.js +2 -2
  2. package/dist/vite/constants.js +1 -0
  3. package/dist/vite/utils/compiler.d.ts +1 -0
  4. package/dist/vite/utils/compiler.js +5 -4
  5. package/dist/vite/utils/get-dashboard-paths.d.ts +5 -0
  6. package/dist/vite/utils/get-dashboard-paths.js +20 -0
  7. package/dist/vite/vite-plugin-dashboard-metadata.js +2 -1
  8. package/dist/vite/vite-plugin-tailwind-source.js +2 -15
  9. package/dist/vite/vite-plugin-translations.d.ts +10 -1
  10. package/dist/vite/vite-plugin-translations.js +156 -45
  11. package/dist/vite/vite-plugin-vendure-dashboard.d.ts +12 -0
  12. package/dist/vite/vite-plugin-vendure-dashboard.js +1 -0
  13. package/lingui.config.js +1 -0
  14. package/package.json +7 -7
  15. package/src/app/routeTree.gen.ts +1221 -0
  16. package/src/app/routes/_authenticated/_administrators/administrators.tsx +9 -12
  17. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +9 -12
  18. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +6 -9
  19. package/src/app/routes/_authenticated/_channels/channels.tsx +9 -12
  20. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +9 -12
  21. package/src/app/routes/_authenticated/_collections/collections.tsx +9 -12
  22. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +9 -12
  23. package/src/app/routes/_authenticated/_countries/countries.tsx +9 -12
  24. package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +9 -12
  25. package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +9 -12
  26. package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +9 -12
  27. package/src/app/routes/_authenticated/_customers/components/customer-history/index.ts +0 -1
  28. package/src/app/routes/_authenticated/_customers/customers.tsx +9 -12
  29. package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +9 -12
  30. package/src/app/routes/_authenticated/_facets/facets.tsx +9 -12
  31. package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +9 -12
  32. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +9 -12
  33. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +10 -13
  34. package/src/app/routes/_authenticated/_orders/components/add-surcharge-form.tsx +139 -0
  35. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +3 -0
  36. package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +3 -1
  37. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +3 -3
  38. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +41 -41
  39. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +1 -1
  40. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +49 -11
  41. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +4 -1
  42. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +2 -3
  43. package/src/app/routes/_authenticated/_orders/orders.tsx +3 -3
  44. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +12 -3
  45. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +27 -30
  46. package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +23 -0
  47. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +9 -12
  48. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +9 -12
  49. package/src/app/routes/_authenticated/_product-variants/components/add-currency-dropdown.tsx +3 -3
  50. package/src/app/routes/_authenticated/_product-variants/components/add-stock-location-dropdown.tsx +2 -2
  51. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +1 -0
  52. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +10 -12
  53. package/src/app/routes/_authenticated/_products/products.graphql.ts +1 -0
  54. package/src/app/routes/_authenticated/_products/products.tsx +15 -18
  55. package/src/app/routes/_authenticated/_products/products_.$id.tsx +9 -12
  56. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +9 -12
  57. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +9 -12
  58. package/src/app/routes/_authenticated/_profile/profile.tsx +3 -3
  59. package/src/app/routes/_authenticated/_promotions/promotions.tsx +9 -12
  60. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +9 -12
  61. package/src/app/routes/_authenticated/_roles/roles.tsx +9 -12
  62. package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +9 -12
  63. package/src/app/routes/_authenticated/_sellers/sellers.tsx +9 -12
  64. package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +9 -12
  65. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +11 -12
  66. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +19 -20
  67. package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +9 -12
  68. package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +9 -12
  69. package/src/app/routes/_authenticated/_system/healthchecks.tsx +2 -3
  70. package/src/app/routes/_authenticated/_system/job-queue.tsx +3 -3
  71. package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +9 -12
  72. package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +9 -12
  73. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +9 -12
  74. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +9 -12
  75. package/src/app/routes/_authenticated/_zones/components/zone-bulk-actions.tsx +49 -1
  76. package/src/app/routes/_authenticated/_zones/components/zone-countries-table.tsx +34 -16
  77. package/src/app/routes/_authenticated/_zones/zones.tsx +9 -12
  78. package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +9 -12
  79. package/src/app/routes/_authenticated/index.tsx +5 -3
  80. package/src/i18n/locales/bg.po +3436 -0
  81. package/src/lib/components/data-input/datetime-input.tsx +1 -1
  82. package/src/lib/components/data-input/default-relation-input.tsx +1 -1
  83. package/src/lib/components/data-input/relation-selector.tsx +1 -1
  84. package/src/lib/components/data-input/string-list-input.tsx +188 -26
  85. package/src/lib/components/data-input/struct-form-input.tsx +175 -174
  86. package/src/lib/components/data-table/column-header-wrapper.tsx +1 -1
  87. package/src/lib/components/data-table/data-table-filter-badge.tsx +2 -2
  88. package/src/lib/components/data-table/data-table.tsx +1 -1
  89. package/src/lib/components/data-table/use-generated-columns.tsx +1 -1
  90. package/src/lib/components/layout/channel-switcher.tsx +6 -2
  91. package/src/lib/components/layout/content-language-selector.tsx +6 -7
  92. package/src/lib/components/layout/dev-mode-indicator.tsx +7 -3
  93. package/src/lib/components/layout/language-dialog.tsx +26 -13
  94. package/src/lib/components/layout/manage-languages-dialog.tsx +10 -29
  95. package/src/lib/components/layout/nav-item-wrapper.tsx +1 -1
  96. package/src/lib/components/shared/asset/asset-gallery.tsx +8 -3
  97. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +14 -16
  98. package/src/lib/components/shared/custom-fields-form.tsx +14 -9
  99. package/src/lib/components/shared/language-selector.tsx +14 -6
  100. package/src/lib/components/shared/multi-select.tsx +1 -1
  101. package/src/lib/components/shared/navigation-confirmation.tsx +1 -1
  102. package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +4 -4
  103. package/src/lib/components/ui/carousel.tsx +2 -2
  104. package/src/lib/components/ui/chart.tsx +1 -1
  105. package/src/lib/components/ui/context-menu.tsx +1 -1
  106. package/src/lib/components/ui/drawer.tsx +1 -1
  107. package/src/lib/components/ui/grid-layout.tsx +1 -1
  108. package/src/lib/components/ui/input-group.tsx +1 -0
  109. package/src/lib/components/ui/input-otp.tsx +1 -1
  110. package/src/lib/components/ui/menubar.tsx +1 -1
  111. package/src/lib/components/ui/navigation-menu.tsx +1 -1
  112. package/src/lib/components/ui/progress.tsx +1 -1
  113. package/src/lib/components/ui/radio-group.tsx +1 -1
  114. package/src/lib/components/ui/resizable.tsx +1 -1
  115. package/src/lib/components/ui/select.tsx +1 -1
  116. package/src/lib/components/ui/slider.tsx +1 -1
  117. package/src/lib/components/ui/toggle-group.tsx +2 -2
  118. package/src/lib/components/ui/toggle.tsx +1 -1
  119. package/src/lib/framework/component-registry/component-registry.tsx +2 -6
  120. package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +907 -1
  121. package/src/lib/framework/document-introspection/add-custom-fields.ts +248 -119
  122. package/src/lib/framework/extension-api/display-component-extensions.tsx +4 -3
  123. package/src/lib/framework/extension-api/logic/detail-forms.ts +0 -13
  124. package/src/lib/framework/extension-api/logic/navigation.ts +1 -1
  125. package/src/lib/framework/extension-api/types/data-table.ts +4 -2
  126. package/src/lib/framework/extension-api/types/layout.ts +34 -1
  127. package/src/lib/framework/extension-api/types/navigation.ts +7 -2
  128. package/src/lib/framework/form-engine/use-generated-form.tsx +7 -1
  129. package/src/lib/framework/history-entry/history-entry.tsx +1 -1
  130. package/src/lib/framework/layout-engine/action-bar-item-wrapper.tsx +185 -0
  131. package/src/lib/framework/layout-engine/dev-mode-button.tsx +15 -13
  132. package/src/lib/framework/layout-engine/location-wrapper.tsx +3 -1
  133. package/src/lib/framework/layout-engine/page-layout.spec.tsx +138 -0
  134. package/src/lib/framework/layout-engine/page-layout.tsx +294 -69
  135. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +1 -1
  136. package/src/lib/framework/page/detail-page-route-loader.tsx +1 -1
  137. package/src/lib/framework/page/page-api.ts +1 -1
  138. package/src/lib/framework/page/use-detail-page.ts +4 -2
  139. package/src/lib/framework/page/use-extended-router.tsx +20 -16
  140. package/src/lib/framework/registry/registry-types.ts +2 -1
  141. package/src/lib/graphql/api.ts +3 -8
  142. package/src/lib/graphql/graphql-env.d.ts +29 -10
  143. package/src/lib/hooks/use-permissions.ts +3 -3
  144. package/src/lib/hooks/use-sorted-languages.ts +41 -0
  145. package/src/lib/index.ts +1 -0
  146. package/src/lib/lib/load-i18n-messages.ts +4 -1
  147. package/src/lib/providers/channel-provider.tsx +11 -7
  148. package/src/lib/utils/config-utils.ts +19 -0
  149. package/src/lib/virtual.d.ts +3 -0
  150. package/LICENSE.md +0 -42
  151. package/src/app/routes/_authenticated/_facets/components/edit-facet-value.tsx +0 -129
  152. /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, only add custom fields to fragments used at the top level
172
- // Skip fragments that are only used in nested contexts
173
- if (topLevelFragments.has(fragmentDef.name.value)) {
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
- let entityType = target.typeName;
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: React.ComponentType<{ value: any; [key: string]: any }>;
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
  }
@@ -10,7 +10,7 @@ export function registerNavigationExtensions(
10
10
  for (const section of navSections) {
11
11
  addNavMenuSection({
12
12
  ...section,
13
- placement: 'top',
13
+ placement: section.placement ?? 'top',
14
14
  order: section.order ?? 999,
15
15
  items: [],
16
16
  });
@@ -1,9 +1,11 @@
1
- import { DataDisplayComponentProps } from '@/vdb/framework/component-registry/component-registry.js';
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: React.ComponentType<DataDisplayComponentProps<CellContext<any, any>>>;
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
- ) => VarName extends keyof VariablesOf<T> ? VariablesOf<T>[VarName] : VariablesOf<T>;
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-danger text-danger-foreground';
37
+ * const destructive = 'bg-destructive text-destructive-foreground';
38
38
  * ```
39
39
  */
40
40
  timelineIconClassName?: string;