@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
@@ -11,7 +11,8 @@ import { CheckIcon, CopyIcon, EllipsisVerticalIcon, InfoIcon } from 'lucide-reac
11
11
  import React, { ComponentProps, useMemo, useState } from 'react';
12
12
  import { Control, UseFormReturn } from 'react-hook-form';
13
13
 
14
- import { DashboardActionBarItem } from '../extension-api/types/layout.js';
14
+ import { ActionBarItemPosition, DashboardActionBarItem } from '../extension-api/types/layout.js';
15
+ import { ActionBarItem, ActionBarItemProps, ActionBarItemWrapper } from './action-bar-item-wrapper.js';
15
16
 
16
17
  import { Button } from '@/vdb/components/ui/button.js';
17
18
  import {
@@ -91,7 +92,7 @@ export interface PageProps extends ComponentProps<'div'> {
91
92
  export function Page({ children, pageId, entity, form, submitHandler, ...props }: Readonly<PageProps>) {
92
93
  const childArray = React.Children.toArray(children);
93
94
 
94
- const pageTitle = childArray.find(child => React.isValidElement(child) && child.type === PageTitle);
95
+ const pageTitle = childArray.find(child => isOfType(child, PageTitle));
95
96
  const pageActionBar = childArray.find(child => isOfType(child, PageActionBar));
96
97
 
97
98
  const pageContent = childArray.filter(
@@ -100,7 +101,7 @@ export function Page({ children, pageId, entity, form, submitHandler, ...props }
100
101
 
101
102
  const pageHeader = (
102
103
  <div className="flex items-center justify-between">
103
- {pageTitle}
104
+ {pageTitle ?? <div />}
104
105
  {pageActionBar}
105
106
  </div>
106
107
  );
@@ -234,34 +235,63 @@ export function PageLayout({ children, className }: Readonly<PageLayoutProps>) {
234
235
  const blockId =
235
236
  childBlock.props.blockId ??
236
237
  (isOfType(childBlock, CustomFieldsPageBlock) ? 'custom-fields' : undefined);
237
- const extensionBlock = extensionBlocks.find(block => block.location.position.blockId === blockId);
238
238
 
239
- if (extensionBlock) {
240
- let extensionBlockShouldRender = true;
241
- if (typeof extensionBlock?.shouldRender === 'function') {
242
- extensionBlockShouldRender = extensionBlock.shouldRender(page);
239
+ // Get all extension blocks with the same position blockId
240
+ const matchingExtensionBlocks = extensionBlocks.filter(
241
+ block => block.location.position.blockId === blockId,
242
+ );
243
+
244
+ // sort the blocks to make sure we have the correct order
245
+ const arrangedExtensionBlocks = matchingExtensionBlocks.sort((a, b) => {
246
+ const orderPriority = { before: 1, replace: 2, after: 3 };
247
+ return orderPriority[a.location.position.order] - orderPriority[b.location.position.order];
248
+ });
249
+
250
+ const replacementBlockExists = arrangedExtensionBlocks.some(
251
+ block => block.location.position.order === 'replace',
252
+ );
253
+
254
+ let childBlockInserted = false;
255
+ if (matchingExtensionBlocks.length > 0) {
256
+ for (const extensionBlock of arrangedExtensionBlocks) {
257
+ let extensionBlockShouldRender = true;
258
+ if (typeof extensionBlock?.shouldRender === 'function') {
259
+ extensionBlockShouldRender = extensionBlock.shouldRender(page);
260
+ }
261
+
262
+ // Insert child block before the first non-"before" block
263
+ if (
264
+ !childBlockInserted &&
265
+ !replacementBlockExists &&
266
+ extensionBlock.location.position.order !== 'before'
267
+ ) {
268
+ finalChildArray.push(childBlock);
269
+ childBlockInserted = true;
270
+ }
271
+
272
+ const isFullWidth = extensionBlock.location.column === 'full';
273
+ const BlockComponent = isFullWidth ? FullWidthPageBlock : PageBlock;
274
+
275
+ const ExtensionBlock =
276
+ extensionBlock.component && extensionBlockShouldRender ? (
277
+ <BlockComponent
278
+ key={extensionBlock.id}
279
+ column={extensionBlock.location.column}
280
+ blockId={extensionBlock.id}
281
+ title={extensionBlock.title}
282
+ >
283
+ {<extensionBlock.component context={page} />}
284
+ </BlockComponent>
285
+ ) : undefined;
286
+
287
+ if (extensionBlockShouldRender && ExtensionBlock) {
288
+ finalChildArray.push(ExtensionBlock);
289
+ }
243
290
  }
244
- const ExtensionBlock =
245
- extensionBlock.component && extensionBlockShouldRender ? (
246
- <PageBlock
247
- key={childBlock.key}
248
- column={extensionBlock.location.column}
249
- blockId={extensionBlock.id}
250
- title={extensionBlock.title}
251
- >
252
- {<extensionBlock.component context={page} />}
253
- </PageBlock>
254
- ) : undefined;
255
- if (extensionBlock.location.position.order === 'before') {
256
- finalChildArray.push(...[ExtensionBlock, childBlock].filter(x => !!x));
257
- } else if (extensionBlock.location.position.order === 'after') {
258
- finalChildArray.push(...[childBlock, ExtensionBlock].filter(x => !!x));
259
- } else if (
260
- extensionBlock.location.position.order === 'replace' &&
261
- extensionBlockShouldRender &&
262
- ExtensionBlock
263
- ) {
264
- finalChildArray.push(ExtensionBlock);
291
+
292
+ // If all blocks were "before", insert child block at the end
293
+ if (!childBlockInserted && !replacementBlockExists) {
294
+ finalChildArray.push(childBlock);
265
295
  }
266
296
  } else {
267
297
  finalChildArray.push(childBlock);
@@ -286,7 +316,7 @@ export function PageLayout({ children, className }: Readonly<PageLayoutProps>) {
286
316
  <div className="@3xl/layout:col-span-1 space-y-4">{sideBlocks}</div>
287
317
  </div>
288
318
  ) : (
289
- <div className="space-y-4">{children}</div>
319
+ <div className="space-y-4">{finalChildArray}</div>
290
320
  )}
291
321
  </div>
292
322
  );
@@ -308,44 +338,226 @@ export function PageTitle({ children }: Readonly<{ children: React.ReactNode }>)
308
338
  return <h1 className="text-2xl font-semibold">{children}</h1>;
309
339
  }
310
340
 
341
+ type InlineDropdownItem = Omit<DashboardActionBarItem, 'type' | 'pageId'>;
342
+
311
343
  /**
312
- * @description *
344
+ * @description
313
345
  * A component for displaying the main actions for a page. This should be used inside the {@link Page} component.
314
- * It should be used in conjunction with the {@link PageActionBarLeft} and {@link PageActionBarRight} components
315
- * as direct children.
346
+ *
347
+ * You can add action bar items by including {@link ActionBarItem} components as direct children.
348
+ * For backwards compatibility, {@link PageActionBarLeft} and {@link PageActionBarRight} are also supported.
349
+ *
350
+ * @example
351
+ * ```tsx
352
+ * <PageActionBar>
353
+ * <ActionBarItem itemId="save-button" requiresPermission={['UpdateProduct']}>
354
+ * <Button type="submit">Update</Button>
355
+ * </ActionBarItem>
356
+ * </PageActionBar>
357
+ * ```
316
358
  *
317
359
  * @docsCategory page-layout
318
360
  * @docsPage PageActionBar
319
361
  * @docsWeight 0
320
362
  * @since 3.3.0
321
363
  */
322
- export function PageActionBar({ children }: Readonly<{ children: React.ReactNode }>) {
323
- let childArray = React.Children.toArray(children);
364
+ export function PageActionBar({
365
+ children,
366
+ dropdownMenuItems,
367
+ }: Readonly<{
368
+ children: React.ReactNode;
369
+ /**
370
+ * @description
371
+ * Optional dropdown menu items to display in the action bar's context menu.
372
+ */
373
+ dropdownMenuItems?: InlineDropdownItem[];
374
+ }>) {
375
+ const page = usePage();
376
+ const actionBarItems = page.pageId ? getDashboardActionBarItems(page.pageId) : [];
377
+ const childArray = React.Children.toArray(children);
324
378
 
379
+ // Extract different child types
325
380
  const leftContent = childArray.filter(child => isOfType(child, PageActionBarLeft));
326
381
  const rightContent = childArray.filter(child => isOfType(child, PageActionBarRight));
327
382
 
383
+ // Collect ActionBarItem children (direct or from PageActionBarRight)
384
+ const actionBarItemChildren: React.ReactElement<ActionBarItemProps>[] = [];
385
+ // Collect plain children (not ActionBarItem, not PageActionBarLeft/Right)
386
+ const plainChildren: React.ReactNode[] = [];
387
+ // Collect dropdownMenuItems from PageActionBarRight (backwards compat)
388
+ let legacyDropdownMenuItems: InlineDropdownItem[] = [];
389
+
390
+ // Direct children (new pattern)
391
+ childArray.forEach(child => {
392
+ if (isActionBarItem(child)) {
393
+ actionBarItemChildren.push(child);
394
+ } else if (!isOfType(child, PageActionBarLeft) && !isOfType(child, PageActionBarRight)) {
395
+ // Plain children (buttons etc.) that aren't ActionBarItem or layout components
396
+ plainChildren.push(child);
397
+ }
398
+ });
399
+
400
+ // Children and dropdownMenuItems from PageActionBarRight (backwards compat)
401
+ rightContent.forEach(rightChild => {
402
+ if (React.isValidElement(rightChild)) {
403
+ const props = rightChild.props as {
404
+ children?: React.ReactNode;
405
+ dropdownMenuItems?: InlineDropdownItem[];
406
+ };
407
+ React.Children.forEach(props.children, child => {
408
+ if (isActionBarItem(child)) {
409
+ actionBarItemChildren.push(child);
410
+ } else {
411
+ // Plain children (raw buttons etc.)
412
+ plainChildren.push(child);
413
+ }
414
+ });
415
+ // Extract dropdownMenuItems from PageActionBarRight props
416
+ if (props.dropdownMenuItems) {
417
+ legacyDropdownMenuItems = [...legacyDropdownMenuItems, ...props.dropdownMenuItems];
418
+ }
419
+ }
420
+ });
421
+
422
+ // Separate button items from dropdown items
423
+ const extensionButtonItems = actionBarItems.filter(item => item.type !== 'dropdown');
424
+ const allDropdownMenuItems = [...(dropdownMenuItems ?? []), ...legacyDropdownMenuItems];
425
+ const actionBarDropdownItems = [
426
+ ...allDropdownMenuItems.map(item => ({
427
+ ...item,
428
+ pageId: page.pageId ?? '',
429
+ type: 'dropdown' as const,
430
+ })),
431
+ ...actionBarItems.filter(item => item.type === 'dropdown'),
432
+ ];
433
+
434
+ // Merge and sort inline items with extension items
435
+ const mergedItems = mergeAndSortActionBarItems(actionBarItemChildren, extensionButtonItems);
436
+
437
+ // Determine if we should render the right section
438
+ const hasRightContent =
439
+ mergedItems.length > 0 ||
440
+ plainChildren.length > 0 ||
441
+ actionBarDropdownItems.length > 0 ||
442
+ page.entity;
443
+
328
444
  return (
329
445
  <div className={cn('flex gap-2', leftContent.length > 0 ? 'justify-between' : 'justify-end')}>
330
446
  {leftContent.length > 0 && <div className="flex justify-start gap-2">{leftContent}</div>}
331
- {rightContent.length > 0 && <div className="flex justify-end gap-2">{rightContent}</div>}
447
+ {hasRightContent && (
448
+ <div className="flex justify-end gap-2">
449
+ {/* Plain children (buttons etc. not wrapped in ActionBarItem) */}
450
+ {plainChildren.map((child, index) => (
451
+ <React.Fragment key={`plain-${index}`}>{child}</React.Fragment>
452
+ ))}
453
+ {/* Merged ActionBarItem children with extensions */}
454
+ {mergedItems.map((mergedItem, index) => {
455
+ if (mergedItem.type === 'inline') {
456
+ return React.cloneElement(mergedItem.element, {
457
+ key: `inline-${mergedItem.element.props.itemId}`,
458
+ });
459
+ } else {
460
+ const extItem = mergedItem.item;
461
+ const itemId = extItem.id ?? `extension-${extItem.component.name || index}`;
462
+ return (
463
+ <ActionBarItemWrapper
464
+ key={`ext-${extItem.id ?? extItem.pageId}-${index}`}
465
+ itemId={itemId}
466
+ >
467
+ <PageActionBarItem item={extItem} page={page} />
468
+ </ActionBarItemWrapper>
469
+ );
470
+ }
471
+ })}
472
+ {actionBarDropdownItems.length > 0 && (
473
+ <PageActionBarDropdown items={actionBarDropdownItems} page={page} />
474
+ )}
475
+ <EntityInfoDropdown entity={page.entity} />
476
+ </div>
477
+ )}
332
478
  </div>
333
479
  );
334
480
  }
335
481
 
336
482
  /**
337
483
  * @description
338
- * The PageActionBarLeft component should be used to display the left content of the action bar.
484
+ * The PageActionBarLeft component is not used and will be removed in a future version.
339
485
  *
340
486
  * @docsCategory page-layout
341
487
  * @docsPage PageActionBar
488
+ * @deprecated
342
489
  * @since 3.3.0
343
490
  */
344
491
  export function PageActionBarLeft({ children }: Readonly<{ children: React.ReactNode }>) {
345
492
  return <div className="flex justify-start gap-2">{children}</div>;
346
493
  }
347
494
 
348
- type InlineDropdownItem = Omit<DashboardActionBarItem, 'type' | 'pageId'>;
495
+ /**
496
+ * Checks if a React child is an ActionBarItem component.
497
+ */
498
+ function isActionBarItem(child: unknown): child is React.ReactElement<ActionBarItemProps> {
499
+ return React.isValidElement(child) && isOfType(child, ActionBarItem);
500
+ }
501
+
502
+ /**
503
+ * Represents a merged action bar item that can be either inline (ActionBarItem child) or from an extension.
504
+ * Used internally for sorting and rendering.
505
+ */
506
+ type MergedActionBarItem =
507
+ | { type: 'inline'; element: React.ReactElement<ActionBarItemProps> }
508
+ | { type: 'extension'; item: DashboardActionBarItem };
509
+
510
+ /**
511
+ * Merges inline ActionBarItem children with extension items, applying position-based ordering.
512
+ * Uses the same priority sorting as page blocks: before=1, replace=2, after=3.
513
+ */
514
+ function mergeAndSortActionBarItems(
515
+ inlineElements: React.ReactElement<ActionBarItemProps>[],
516
+ extensionItems: DashboardActionBarItem[],
517
+ ): MergedActionBarItem[] {
518
+ const result: MergedActionBarItem[] = [];
519
+
520
+ // First, add extension items WITHOUT a position (they go first, preserving current behavior)
521
+ const unpositionedExtensions = extensionItems.filter(ext => !ext.position);
522
+ for (const ext of unpositionedExtensions) {
523
+ result.push({ type: 'extension', item: ext });
524
+ }
525
+
526
+ // Process each inline element and find extension items targeting it
527
+ for (const inlineElement of inlineElements) {
528
+ const itemId = inlineElement.props.itemId;
529
+ const matchingExtensions = extensionItems.filter(ext => ext.position?.itemId === itemId);
530
+
531
+ // Sort by order priority: before=1, replace=2, after=3
532
+ const sortedExtensions = matchingExtensions.sort((a, b) => {
533
+ const orderPriority: Record<ActionBarItemPosition['order'], number> = {
534
+ before: 1,
535
+ replace: 2,
536
+ after: 3,
537
+ };
538
+ return orderPriority[a.position!.order] - orderPriority[b.position!.order];
539
+ });
540
+
541
+ const hasReplacement = sortedExtensions.some(ext => ext.position?.order === 'replace');
542
+
543
+ let inlineInserted = false;
544
+ for (const ext of sortedExtensions) {
545
+ // Insert inline element before the first non-"before" extension (if not replaced)
546
+ if (!inlineInserted && !hasReplacement && ext.position?.order !== 'before') {
547
+ result.push({ type: 'inline', element: inlineElement });
548
+ inlineInserted = true;
549
+ }
550
+ result.push({ type: 'extension', item: ext });
551
+ }
552
+
553
+ // If all extensions were "before" or there were no extensions, add inline at the end
554
+ if (!inlineInserted && !hasReplacement) {
555
+ result.push({ type: 'inline', element: inlineElement });
556
+ }
557
+ }
558
+
559
+ return result;
560
+ }
349
561
 
350
562
  function EntityInfoDropdown({ entity }: Readonly<{ entity: any }>) {
351
563
  const [copiedField, setCopiedField] = useState<string | null>(null);
@@ -428,7 +640,28 @@ function EntityInfoDropdown({ entity }: Readonly<{ entity: any }>) {
428
640
 
429
641
  /**
430
642
  * @description
431
- * The PageActionBarRight component should be used to display the right content of the action bar.
643
+ * The PageActionBarRight component is used to display the right content of the action bar.
644
+ *
645
+ * @deprecated Use {@link ActionBarItem} children directly in {@link PageActionBar} instead.
646
+ *
647
+ * @example
648
+ * ```tsx
649
+ * // Old pattern (deprecated)
650
+ * <PageActionBar>
651
+ * <PageActionBarRight>
652
+ * <ActionBarItem itemId="save-button">
653
+ * <Button type="submit">Update</Button>
654
+ * </ActionBarItem>
655
+ * </PageActionBarRight>
656
+ * </PageActionBar>
657
+ *
658
+ * // New pattern (recommended)
659
+ * <PageActionBar>
660
+ * <ActionBarItem itemId="save-button" requiresPermission={['UpdateProduct']}>
661
+ * <Button type="submit">Update</Button>
662
+ * </ActionBarItem>
663
+ * </PageActionBar>
664
+ * ```
432
665
  *
433
666
  * @docsCategory page-layout
434
667
  * @docsPage PageActionBar
@@ -436,35 +669,25 @@ function EntityInfoDropdown({ entity }: Readonly<{ entity: any }>) {
436
669
  */
437
670
  export function PageActionBarRight({
438
671
  children,
439
- dropdownMenuItems,
672
+ dropdownMenuItems: _dropdownMenuItems,
440
673
  }: Readonly<{
441
- children: React.ReactNode;
674
+ /**
675
+ * @description
676
+ * ActionBarItem components that will be rendered in the action bar.
677
+ * Each item should have a unique `itemId` for extension targeting.
678
+ */
679
+ children?: React.ReactNode;
680
+ /**
681
+ * @description
682
+ * Optional dropdown menu items. These are now extracted and rendered by PageActionBar.
683
+ * @deprecated Pass `dropdownMenuItems` directly to {@link PageActionBar} instead.
684
+ */
442
685
  dropdownMenuItems?: InlineDropdownItem[];
443
686
  }>) {
444
- const page = usePage();
445
- const actionBarItems = page.pageId ? getDashboardActionBarItems(page.pageId) : [];
446
- const actionBarButtonItems = actionBarItems.filter(item => item.type !== 'dropdown');
447
- const actionBarDropdownItems = [
448
- ...(dropdownMenuItems ?? []).map(item => ({
449
- ...item,
450
- pageId: page.pageId ?? '',
451
- type: 'dropdown' as const,
452
- })),
453
- ...actionBarItems.filter(item => item.type === 'dropdown'),
454
- ];
455
-
456
- return (
457
- <div className="flex justify-end gap-2">
458
- {actionBarButtonItems.map((item, index) => (
459
- <PageActionBarItem key={item.pageId + index} item={item} page={page} />
460
- ))}
461
- {children}
462
- {actionBarDropdownItems.length > 0 && (
463
- <PageActionBarDropdown items={actionBarDropdownItems} page={page} />
464
- )}
465
- <EntityInfoDropdown entity={page.entity} />
466
- </div>
467
- );
687
+ // This is now a passthrough wrapper for backwards compatibility.
688
+ // The actual logic is handled by PageActionBar which extracts ActionBarItem
689
+ // children and dropdownMenuItems from PageActionBarRight.
690
+ return <>{children}</>;
468
691
  }
469
692
 
470
693
  function PageActionBarItem({
@@ -518,7 +741,7 @@ export type PageBlockProps = {
518
741
  * @description
519
742
  * Which column this block should appear in
520
743
  */
521
- column: 'main' | 'side';
744
+ column: 'main' | 'side' | 'full';
522
745
  /**
523
746
  * @description
524
747
  * The ID of the block, e.g. "gift-cards" or "related-products".
@@ -586,9 +809,7 @@ export function PageBlock({
586
809
  {description && <CardDescription>{description}</CardDescription>}
587
810
  </CardHeader>
588
811
  ) : null}
589
- <CardContent className={cn(!title ? 'pt-6' : '', 'overflow-auto')}>
590
- {children}
591
- </CardContent>
812
+ <CardContent className={cn(!title ? 'pt-6' : '', '')}>{children}</CardContent>
592
813
  </Card>
593
814
  </LocationWrapper>
594
815
  </PageBlockContext.Provider>
@@ -669,3 +890,7 @@ export function isOfType(el: unknown, type: React.FunctionComponent<any>): boole
669
890
  }
670
891
  return false;
671
892
  }
893
+
894
+ // Re-export ActionBarItem for convenience alongside other page layout components
895
+ export { ActionBarItem } from './action-bar-item-wrapper.js';
896
+ export type { ActionBarItemProps } from './action-bar-item-wrapper.js';
@@ -13,7 +13,7 @@ export type NavMenuSectionPlacement = 'top' | 'bottom';
13
13
  * @docsPage Navigation
14
14
  * @since 3.4.0
15
15
  */
16
- interface NavMenuItem {
16
+ export interface NavMenuItem {
17
17
  /**
18
18
  * @description
19
19
  * A unique ID for this nav menu item
@@ -29,7 +29,7 @@ export function detailPageRouteLoader<T extends TypedDocumentNode<any, any>>({
29
29
  queryDocument,
30
30
  breadcrumb,
31
31
  }: DetailPageRouteLoaderConfig<T>) {
32
- const loader: FileBaseRouteOptions<any, any>['loader'] = async ({
32
+ const loader: FileBaseRouteOptions<any, any, any>['loader'] = async ({
33
33
  context,
34
34
  params,
35
35
  location,
@@ -1,4 +1,4 @@
1
- import { DashboardRouteDefinition } from '../extension-api/extension-api-types.js';
1
+ import { DashboardRouteDefinition } from '../extension-api/types/navigation.js';
2
2
 
3
3
  export const extensionRoutes = new Map<string, DashboardRouteDefinition>();
4
4
 
@@ -22,7 +22,7 @@ import {
22
22
  getMutationName,
23
23
  getQueryName,
24
24
  } from '../document-introspection/get-document-structure.js';
25
- import { useGeneratedForm } from '../form-engine/use-generated-form.js';
25
+ import { useGeneratedForm, WithLooseCustomFields } from '../form-engine/use-generated-form.js';
26
26
 
27
27
  import { DetailEntityPath } from './page-types.js';
28
28
 
@@ -95,7 +95,9 @@ export interface DetailPageOptions<
95
95
  * @description
96
96
  * The function to set the values for the update document.
97
97
  */
98
- setValuesForUpdate: (entity: NonNullable<ResultOf<T>[EntityField]>) => VariablesOf<U>[VarNameUpdate];
98
+ setValuesForUpdate: (
99
+ entity: NonNullable<ResultOf<T>[EntityField]>,
100
+ ) => WithLooseCustomFields<VariablesOf<U>[VarNameUpdate]>;
99
101
  transformCreateInput?: (input: VariablesOf<C>[VarNameCreate]) => VariablesOf<C>[VarNameCreate];
100
102
  transformUpdateInput?: (input: VariablesOf<U>[VarNameUpdate]) => VariablesOf<U>[VarNameUpdate];
101
103
  /**
@@ -21,10 +21,7 @@ export const useExtendedRouter = (
21
21
 
22
22
  // Only extend if extensions are loaded
23
23
  if (!extensionsLoaded) {
24
- return createRouter({
25
- ...routerOptions,
26
- routeTree,
27
- });
24
+ return createExtendedRouter(routerOptions, routeTree);
28
25
  }
29
26
 
30
27
  const authenticatedRouteIndex = routeTree.children.findIndex(
@@ -33,10 +30,7 @@ export const useExtendedRouter = (
33
30
 
34
31
  if (authenticatedRouteIndex === -1) {
35
32
  // No authenticated route found, return router with base tree
36
- return createRouter({
37
- ...routerOptions,
38
- routeTree,
39
- });
33
+ return createExtendedRouter(routerOptions, routeTree);
40
34
  }
41
35
 
42
36
  let authenticatedRoute: AnyRoute = routeTree.children[authenticatedRouteIndex];
@@ -106,10 +100,7 @@ export const useExtendedRouter = (
106
100
 
107
101
  // Only extend the tree if we have new routes to add
108
102
  if (newAuthenticatedRoutes.length === 0 && newRootRoutes.length === 0) {
109
- return createRouter({
110
- ...routerOptions,
111
- routeTree,
112
- });
103
+ return createExtendedRouter(routerOptions, routeTree);
113
104
  }
114
105
 
115
106
  const childrenWithoutAuthenticated = routeTree.children.filter(
@@ -127,9 +118,22 @@ export const useExtendedRouter = (
127
118
  ...newRootRoutes,
128
119
  ]);
129
120
 
130
- return createRouter({
131
- ...routerOptions,
132
- routeTree: extendedRouteTree,
133
- });
121
+ return createExtendedRouter(routerOptions, extendedRouteTree);
134
122
  }, [baseRouteTree, routerOptions, extensionsLoaded]);
135
123
  };
124
+
125
+ /**
126
+ * Helper to create a router with extended route tree, handling some
127
+ * type issues with hydrate/dehydrate functions.
128
+ */
129
+ function createExtendedRouter(
130
+ routerOptions: Omit<RouterOptions<AnyRoute, any>, 'routeTree'>,
131
+ extendedRouteTree: AnyRoute,
132
+ ) {
133
+ return createRouter({
134
+ ...routerOptions,
135
+ dehydrate: routerOptions.dehydrate as any,
136
+ hydrate: routerOptions.hydrate as any,
137
+ routeTree: extendedRouteTree,
138
+ });
139
+ }
@@ -11,6 +11,7 @@ import { DocumentNode } from 'graphql';
11
11
 
12
12
  import { DataDisplayComponent } from '../component-registry/component-registry.js';
13
13
  import { DashboardAlertDefinition } from '../extension-api/types/alerts.js';
14
+ import { DataTableDisplayComponent } from '../extension-api/types/data-table.js';
14
15
  import { NavMenuConfig } from '../nav-menu/nav-menu-extensions.js';
15
16
 
16
17
  export interface GlobalRegistryContents {
@@ -22,7 +23,7 @@ export interface GlobalRegistryContents {
22
23
  dashboardWidgetRegistry: Map<string, DashboardWidgetDefinition>;
23
24
  dashboardAlertRegistry: Map<string, DashboardAlertDefinition>;
24
25
  inputComponents: Map<string, DashboardFormComponent>;
25
- displayComponents: Map<string, DataDisplayComponent>;
26
+ displayComponents: Map<string, DataDisplayComponent | DataTableDisplayComponent>;
26
27
  bulkActionsRegistry: Map<string, BulkAction[]>;
27
28
  listQueryDocumentRegistry: Map<string, DocumentNode[]>;
28
29
  detailQueryDocumentRegistry: Map<string, DocumentNode[]>;
@@ -8,14 +8,9 @@ import { AwesomeGraphQLClient } from 'awesome-graphql-client';
8
8
  import { DocumentNode, print } from 'graphql';
9
9
  import { uiConfig } from 'virtual:vendure-ui-config';
10
10
 
11
- const host =
12
- uiConfig.api.host !== 'auto'
13
- ? uiConfig.api.host
14
- : `${window.location.protocol}//${window.location.hostname}`;
15
- const API_URL =
16
- host +
17
- `:${uiConfig.api.port !== 'auto' ? uiConfig.api.port : window.location.port}` +
18
- `/${uiConfig.api.adminApiPath}`;
11
+ import { getApiBaseUrl } from '../utils/config-utils.js';
12
+
13
+ const API_URL = getApiBaseUrl() + `/${uiConfig.api.adminApiPath}`;
19
14
 
20
15
  export type Variables = object;
21
16
  export type RequestDocument = string | DocumentNode;