@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
@@ -0,0 +1,185 @@
1
+ import { CopyableText } from '@/vdb/components/shared/copyable-text.js';
2
+ import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
3
+ import { Popover, PopoverContent, PopoverTrigger } from '@/vdb/components/ui/popover.js';
4
+ import { usePage } from '@/vdb/hooks/use-page.js';
5
+ import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
6
+ import { cn } from '@/vdb/lib/utils.js';
7
+ import React, { useEffect, useState } from 'react';
8
+ import { DevModeButton } from './dev-mode-button.js';
9
+
10
+ // Singleton state for hover tracking across all action bar items
11
+ let globalHoveredActionBarItemId: string | null = null;
12
+ const actionBarHoverListeners: Set<(id: string | null) => void> = new Set();
13
+
14
+ const setGlobalHoveredActionBarItemId = (id: string | null) => {
15
+ globalHoveredActionBarItemId = id;
16
+ actionBarHoverListeners.forEach(listener => listener(id));
17
+ };
18
+
19
+ /**
20
+ * Internal component that renders the dev-mode wrapper with hover highlight and popover.
21
+ * Shared between ActionBarItem and ActionBarItemWrapper to eliminate duplication.
22
+ */
23
+ function DevModeActionBarWrapper({
24
+ children,
25
+ itemId,
26
+ }: Readonly<{
27
+ children: React.ReactNode;
28
+ itemId: string;
29
+ }>) {
30
+ const page = usePage();
31
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
32
+ const [hoveredId, setHoveredId] = useState<string | null>(globalHoveredActionBarItemId);
33
+
34
+ // Generate a unique tracking ID that includes the page context
35
+ const trackingId = `${page.pageId ?? 'unknown'}-actionbar-${itemId}`;
36
+ const isHovered = hoveredId === trackingId;
37
+
38
+ // Subscribe to global hover changes
39
+ useEffect(() => {
40
+ const listener = (newHoveredId: string | null) => {
41
+ setHoveredId(newHoveredId);
42
+ };
43
+ actionBarHoverListeners.add(listener);
44
+ return () => {
45
+ actionBarHoverListeners.delete(listener);
46
+ };
47
+ }, []);
48
+
49
+ const handleMouseEnter = () => {
50
+ setGlobalHoveredActionBarItemId(trackingId);
51
+ };
52
+
53
+ const handleMouseLeave = () => {
54
+ setGlobalHoveredActionBarItemId(null);
55
+ };
56
+
57
+ return (
58
+ <div
59
+ className={cn(
60
+ 'ring-1 ring-transparent rounded transition-all delay-50 relative',
61
+ isHovered || isPopoverOpen ? 'ring-dev-mode ring-offset-1 ring-offset-background' : '',
62
+ )}
63
+ onMouseEnter={handleMouseEnter}
64
+ onMouseLeave={handleMouseLeave}
65
+ >
66
+ <div
67
+ className={cn(
68
+ 'absolute -top-1 -right-1 transition-all delay-50 z-10',
69
+ isHovered || isPopoverOpen ? 'visible' : 'invisible',
70
+ )}
71
+ >
72
+ <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
73
+ <PopoverTrigger asChild>
74
+ <DevModeButton className="h-5 w-5 top-0 -start-4" />
75
+ </PopoverTrigger>
76
+ <PopoverContent className="w-40 p-2">
77
+ <div className="space-y-1.5">
78
+ {page.pageId && (
79
+ <div className="text-xs">
80
+ <div className="text-muted-foreground mb-0.5">pageId</div>
81
+ <CopyableText text={page.pageId} />
82
+ </div>
83
+ )}
84
+ <div className="text-xs">
85
+ <div className="text-muted-foreground mb-0.5">itemId</div>
86
+ <CopyableText text={itemId} />
87
+ </div>
88
+ </div>
89
+ </PopoverContent>
90
+ </Popover>
91
+ </div>
92
+ {children}
93
+ </div>
94
+ );
95
+ }
96
+
97
+ /**
98
+ * @description
99
+ * Props for the ActionBarItem component.
100
+ *
101
+ * @docsCategory page-layout
102
+ * @docsPage PageActionBar
103
+ * @since 3.5.2
104
+ */
105
+ export interface ActionBarItemProps {
106
+ /**
107
+ * @description
108
+ * The content of the action bar item, typically a Button component.
109
+ */
110
+ children: React.ReactNode;
111
+ /**
112
+ * @description
113
+ * A unique identifier for this action bar item. This ID is used by extensions
114
+ * to position their items relative to this one via `position.itemId`.
115
+ *
116
+ * Note: Extensions should use this exact `itemId` value in their `position.itemId`
117
+ * field to target this item.
118
+ */
119
+ itemId: string;
120
+ /**
121
+ * @description
122
+ * If provided, the logged-in user must have one or more of the specified
123
+ * permissions in order for the item to render.
124
+ */
125
+ requiresPermission?: string | string[];
126
+ }
127
+
128
+ /**
129
+ * @description
130
+ * A component for wrapping action bar items with a unique ID. This should be used inside
131
+ * the {@link PageActionBarRight} component. Each item is given an `itemId` which allows
132
+ * extensions to position their items relative to it using `position.itemId`.
133
+ *
134
+ * In developer mode, hovering over the item will show a popover with the `pageId` and `itemId`,
135
+ * making it easy to discover the correct IDs for extension positioning.
136
+ *
137
+ * @example
138
+ * ```tsx
139
+ * <PageActionBarRight>
140
+ * <ActionBarItem itemId="save-button" requiresPermission={['UpdateProduct']}>
141
+ * <Button type="submit">Save</Button>
142
+ * </ActionBarItem>
143
+ * </PageActionBarRight>
144
+ * ```
145
+ *
146
+ * @docsCategory page-layout
147
+ * @docsPage PageActionBar
148
+ * @since 3.5.2
149
+ */
150
+ export function ActionBarItem({ children, itemId, requiresPermission }: Readonly<ActionBarItemProps>) {
151
+ const { settings } = useUserSettings();
152
+
153
+ const content = requiresPermission ? (
154
+ <PermissionGuard requires={requiresPermission}>{children}</PermissionGuard>
155
+ ) : (
156
+ children
157
+ );
158
+
159
+ if (settings.devMode) {
160
+ return <DevModeActionBarWrapper itemId={itemId}>{content}</DevModeActionBarWrapper>;
161
+ }
162
+ return <>{content}</>;
163
+ }
164
+
165
+ /**
166
+ * Internal wrapper component used by PageActionBarRight to wrap extension items
167
+ * with dev-mode location information. Unlike ActionBarItem, this does not handle
168
+ * permissions (those are handled by PageActionBarItem).
169
+ *
170
+ * @internal
171
+ */
172
+ export function ActionBarItemWrapper({
173
+ children,
174
+ itemId,
175
+ }: Readonly<{
176
+ children: React.ReactNode;
177
+ itemId: string;
178
+ }>) {
179
+ const { settings } = useUserSettings();
180
+
181
+ if (settings.devMode) {
182
+ return <DevModeActionBarWrapper itemId={itemId}>{children}</DevModeActionBarWrapper>;
183
+ }
184
+ return <>{children}</>;
185
+ }
@@ -1,24 +1,26 @@
1
1
  import { Button } from '@/vdb/components/ui/button.js';
2
2
  import { cn } from '@/vdb/lib/utils.js';
3
- import { CodeXmlIcon } from 'lucide-react';
3
+ import { Locate } from 'lucide-react';
4
4
  import { forwardRef } from 'react';
5
5
 
6
6
  export const DevModeButton = forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
7
7
  (props, ref) => {
8
8
  const { className, ...rest } = props;
9
9
  return (
10
- <Button
11
- ref={ref}
12
- variant="secondary"
13
- size="icon"
14
- className={cn(
15
- 'h-8 w-8 rounded-full bg-dev-mode/20 hover:bg-dev-mode/30 border border-dev-mode/20 shadow-sm',
16
- className,
17
- )}
18
- {...rest}
19
- >
20
- <CodeXmlIcon className="text-dev-mode w-4 h-4" />
21
- </Button>
10
+ <div className="relative">
11
+ <Button
12
+ ref={ref}
13
+ variant="secondary"
14
+ size="icon"
15
+ className={cn(
16
+ 'h-6 w-6 absolute z-50 rounded-md bg-background text-dev-mode/70 hover:bg-background hover:text-dev-mode border border-dev-mode shadow-sm',
17
+ className,
18
+ )}
19
+ {...rest}
20
+ >
21
+ <Locate className="w-4 h-4" />
22
+ </Button>
23
+ </div>
22
24
  );
23
25
  },
24
26
  );
@@ -86,7 +86,9 @@ export function LocationWrapper({ children, identifier }: Readonly<LocationWrapp
86
86
  >
87
87
  <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
88
88
  <PopoverTrigger asChild>
89
- <DevModeButton />
89
+ <DevModeButton
90
+ className={isPageWrapper ? '-top-8 -end-1 border-2' : '-top-4 -end-4'}
91
+ />
90
92
  </PopoverTrigger>
91
93
  <PopoverContent className="w-48 p-3">
92
94
  <div className="space-y-2">
@@ -0,0 +1,138 @@
1
+ import React from 'react';
2
+ import { renderToStaticMarkup } from 'react-dom/server';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { PageBlock, PageLayout } from './page-layout.js';
6
+ import { registerDashboardPageBlock } from './layout-extensions.js';
7
+ import { PageContext } from './page-provider.js';
8
+ import { globalRegistry } from '../registry/global-registry.js';
9
+ import { UserSettingsContext, type UserSettingsContextType } from '../../providers/user-settings.js';
10
+
11
+ const useMediaQueryMock = vi.hoisted(() => vi.fn());
12
+ const useCopyToClipboardMock = vi.hoisted(() => vi.fn(() => [null, vi.fn()]));
13
+
14
+ vi.mock('@uidotdev/usehooks', () => ({
15
+ useMediaQuery: useMediaQueryMock,
16
+ useCopyToClipboard: useCopyToClipboardMock,
17
+ }));
18
+
19
+ function registerBlock(
20
+ id: string,
21
+ order: 'before' | 'after' | 'replace',
22
+ pageId = 'customer-list',
23
+ ): void {
24
+ registerDashboardPageBlock({
25
+ id,
26
+ title: id,
27
+ location: {
28
+ pageId,
29
+ column: 'main',
30
+ position: { blockId: 'list-table', order },
31
+ },
32
+ component: ({ context }) => <div data-testid={`page-block-${id}`}>{context.pageId}</div>,
33
+ });
34
+ }
35
+
36
+ function renderPageLayout(children: React.ReactNode, { isDesktop = true } = {}) {
37
+ useMediaQueryMock.mockReturnValue(isDesktop);
38
+ const noop = () => undefined;
39
+ const contextValue = {
40
+ settings: {
41
+ displayLanguage: 'en',
42
+ contentLanguage: 'en',
43
+ theme: 'system',
44
+ displayUiExtensionPoints: false,
45
+ mainNavExpanded: true,
46
+ activeChannelId: '',
47
+ devMode: false,
48
+ hasSeenOnboarding: false,
49
+ tableSettings: {},
50
+ },
51
+ settingsStoreIsAvailable: true,
52
+ setDisplayLanguage: noop,
53
+ setDisplayLocale: noop,
54
+ setContentLanguage: noop,
55
+ setTheme: noop,
56
+ setDisplayUiExtensionPoints: noop,
57
+ setMainNavExpanded: noop,
58
+ setActiveChannelId: noop,
59
+ setDevMode: noop,
60
+ setHasSeenOnboarding: noop,
61
+ setTableSettings: () => undefined,
62
+ setWidgetLayout: noop,
63
+ } as UserSettingsContextType;
64
+
65
+ return renderToStaticMarkup(
66
+ <UserSettingsContext.Provider value={contextValue}>
67
+ <PageContext.Provider value={{ pageId: 'customer-list' }}>
68
+ <PageLayout>{children}</PageLayout>
69
+ </PageContext.Provider>
70
+ </UserSettingsContext.Provider>,
71
+ );
72
+ }
73
+
74
+ function getRenderedBlockIds(markup: string) {
75
+ return Array.from(markup.matchAll(/data-testid="(page-block-[^"]+)"/g)).map(match => match[1]);
76
+ }
77
+
78
+ describe('PageLayout', () => {
79
+ beforeEach(() => {
80
+ useMediaQueryMock.mockReset();
81
+ useCopyToClipboardMock.mockReset();
82
+ useCopyToClipboardMock.mockReturnValue([null, vi.fn()]);
83
+ const pageBlockRegistry = globalRegistry.get('dashboardPageBlockRegistry');
84
+ pageBlockRegistry.clear();
85
+ });
86
+
87
+ it('renders multiple before/after extension blocks in registration order', () => {
88
+ registerBlock('before-1', 'before');
89
+ registerBlock('before-2', 'before');
90
+ registerBlock('after-1', 'after');
91
+
92
+ const markup = renderPageLayout(
93
+ <PageBlock column="main" blockId="list-table">
94
+ <div data-testid="page-block-original">original</div>
95
+ </PageBlock>,
96
+ { isDesktop: true },
97
+ );
98
+
99
+ expect(getRenderedBlockIds(markup)).toEqual([
100
+ 'page-block-before-1',
101
+ 'page-block-before-2',
102
+ 'page-block-original',
103
+ 'page-block-after-1',
104
+ ]);
105
+ });
106
+
107
+ it('replaces original block when replacement extensions are registered', () => {
108
+ registerBlock('replacement-1', 'replace');
109
+ registerBlock('replacement-2', 'replace');
110
+
111
+ const markup = renderPageLayout(
112
+ <PageBlock column="main" blockId="list-table">
113
+ <div data-testid="page-block-original">original</div>
114
+ </PageBlock>,
115
+ { isDesktop: true },
116
+ );
117
+
118
+ expect(getRenderedBlockIds(markup)).toEqual(['page-block-replacement-1', 'page-block-replacement-2']);
119
+ });
120
+
121
+ it('renders extension blocks in mobile layout', () => {
122
+ registerBlock('before-mobile', 'before');
123
+ registerBlock('after-mobile', 'after');
124
+
125
+ const markup = renderPageLayout(
126
+ <PageBlock column="main" blockId="list-table">
127
+ <div data-testid="page-block-original">original</div>
128
+ </PageBlock>,
129
+ { isDesktop: false },
130
+ );
131
+
132
+ expect(getRenderedBlockIds(markup)).toEqual([
133
+ 'page-block-before-mobile',
134
+ 'page-block-original',
135
+ 'page-block-after-mobile',
136
+ ]);
137
+ });
138
+ });