@vendure/dashboard 3.6.0-minor-202511061555 → 3.6.0-minor-202512161252
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin/constants.js +2 -2
- package/dist/vite/constants.js +1 -0
- package/dist/vite/utils/compiler.d.ts +1 -0
- package/dist/vite/utils/compiler.js +5 -4
- package/dist/vite/utils/get-dashboard-paths.d.ts +5 -0
- package/dist/vite/utils/get-dashboard-paths.js +20 -0
- package/dist/vite/vite-plugin-dashboard-metadata.js +2 -1
- package/dist/vite/vite-plugin-tailwind-source.js +2 -15
- package/dist/vite/vite-plugin-translations.d.ts +10 -1
- package/dist/vite/vite-plugin-translations.js +156 -45
- package/dist/vite/vite-plugin-vendure-dashboard.d.ts +12 -0
- package/dist/vite/vite-plugin-vendure-dashboard.js +1 -0
- package/lingui.config.js +1 -0
- package/package.json +7 -7
- package/src/app/routeTree.gen.ts +1221 -0
- package/src/app/routes/_authenticated/_administrators/administrators.tsx +9 -12
- package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +6 -9
- package/src/app/routes/_authenticated/_channels/channels.tsx +9 -12
- package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_collections/collections.tsx +9 -12
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_countries/countries.tsx +9 -12
- package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +9 -12
- package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_customers/components/customer-history/index.ts +0 -1
- package/src/app/routes/_authenticated/_customers/customers.tsx +9 -12
- package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_facets/facets.tsx +9 -12
- package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +10 -13
- package/src/app/routes/_authenticated/_orders/components/add-surcharge-form.tsx +139 -0
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +3 -0
- package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +3 -1
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +3 -3
- package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +41 -41
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +1 -1
- package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +49 -11
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +4 -1
- package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +2 -3
- package/src/app/routes/_authenticated/_orders/orders.tsx +3 -3
- package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +12 -3
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +27 -30
- package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +23 -0
- package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +9 -12
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_product-variants/components/add-currency-dropdown.tsx +3 -3
- package/src/app/routes/_authenticated/_product-variants/components/add-stock-location-dropdown.tsx +2 -2
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +10 -12
- package/src/app/routes/_authenticated/_products/products.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_products/products.tsx +15 -18
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_profile/profile.tsx +3 -3
- package/src/app/routes/_authenticated/_promotions/promotions.tsx +9 -12
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_roles/roles.tsx +9 -12
- package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_sellers/sellers.tsx +9 -12
- package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +11 -12
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +19 -20
- package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +9 -12
- package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_system/healthchecks.tsx +2 -3
- package/src/app/routes/_authenticated/_system/job-queue.tsx +3 -3
- package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +9 -12
- package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +9 -12
- package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_zones/components/zone-bulk-actions.tsx +49 -1
- package/src/app/routes/_authenticated/_zones/components/zone-countries-table.tsx +34 -16
- package/src/app/routes/_authenticated/_zones/zones.tsx +9 -12
- package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/index.tsx +5 -3
- package/src/i18n/locales/bg.po +3436 -0
- package/src/lib/components/data-input/datetime-input.tsx +1 -1
- package/src/lib/components/data-input/default-relation-input.tsx +1 -1
- package/src/lib/components/data-input/relation-selector.tsx +1 -1
- package/src/lib/components/data-input/string-list-input.tsx +188 -26
- package/src/lib/components/data-input/struct-form-input.tsx +175 -174
- package/src/lib/components/data-table/column-header-wrapper.tsx +1 -1
- package/src/lib/components/data-table/data-table-filter-badge.tsx +2 -2
- package/src/lib/components/data-table/data-table.tsx +1 -1
- package/src/lib/components/data-table/use-generated-columns.tsx +1 -1
- package/src/lib/components/layout/channel-switcher.tsx +6 -2
- package/src/lib/components/layout/content-language-selector.tsx +6 -7
- package/src/lib/components/layout/dev-mode-indicator.tsx +7 -3
- package/src/lib/components/layout/language-dialog.tsx +26 -13
- package/src/lib/components/layout/manage-languages-dialog.tsx +10 -29
- package/src/lib/components/layout/nav-item-wrapper.tsx +1 -1
- package/src/lib/components/shared/asset/asset-gallery.tsx +8 -3
- package/src/lib/components/shared/configurable-operation-multi-selector.tsx +14 -16
- package/src/lib/components/shared/custom-fields-form.tsx +14 -9
- package/src/lib/components/shared/language-selector.tsx +14 -6
- package/src/lib/components/shared/multi-select.tsx +1 -1
- package/src/lib/components/shared/navigation-confirmation.tsx +1 -1
- package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +4 -4
- package/src/lib/components/ui/carousel.tsx +2 -2
- package/src/lib/components/ui/chart.tsx +1 -1
- package/src/lib/components/ui/context-menu.tsx +1 -1
- package/src/lib/components/ui/drawer.tsx +1 -1
- package/src/lib/components/ui/grid-layout.tsx +1 -1
- package/src/lib/components/ui/input-group.tsx +1 -0
- package/src/lib/components/ui/input-otp.tsx +1 -1
- package/src/lib/components/ui/menubar.tsx +1 -1
- package/src/lib/components/ui/navigation-menu.tsx +1 -1
- package/src/lib/components/ui/progress.tsx +1 -1
- package/src/lib/components/ui/radio-group.tsx +1 -1
- package/src/lib/components/ui/resizable.tsx +1 -1
- package/src/lib/components/ui/select.tsx +1 -1
- package/src/lib/components/ui/slider.tsx +1 -1
- package/src/lib/components/ui/toggle-group.tsx +2 -2
- package/src/lib/components/ui/toggle.tsx +1 -1
- package/src/lib/framework/component-registry/component-registry.tsx +2 -6
- package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +907 -1
- package/src/lib/framework/document-introspection/add-custom-fields.ts +248 -119
- package/src/lib/framework/extension-api/display-component-extensions.tsx +4 -3
- package/src/lib/framework/extension-api/logic/detail-forms.ts +0 -13
- package/src/lib/framework/extension-api/logic/navigation.ts +1 -1
- package/src/lib/framework/extension-api/types/data-table.ts +4 -2
- package/src/lib/framework/extension-api/types/layout.ts +34 -1
- package/src/lib/framework/extension-api/types/navigation.ts +7 -2
- package/src/lib/framework/form-engine/use-generated-form.tsx +7 -1
- package/src/lib/framework/history-entry/history-entry.tsx +1 -1
- package/src/lib/framework/layout-engine/action-bar-item-wrapper.tsx +185 -0
- package/src/lib/framework/layout-engine/dev-mode-button.tsx +15 -13
- package/src/lib/framework/layout-engine/location-wrapper.tsx +3 -1
- package/src/lib/framework/layout-engine/page-layout.spec.tsx +138 -0
- package/src/lib/framework/layout-engine/page-layout.tsx +294 -69
- package/src/lib/framework/nav-menu/nav-menu-extensions.ts +1 -1
- package/src/lib/framework/page/detail-page-route-loader.tsx +1 -1
- package/src/lib/framework/page/page-api.ts +1 -1
- package/src/lib/framework/page/use-detail-page.ts +4 -2
- package/src/lib/framework/page/use-extended-router.tsx +20 -16
- package/src/lib/framework/registry/registry-types.ts +2 -1
- package/src/lib/graphql/api.ts +3 -8
- package/src/lib/graphql/graphql-env.d.ts +29 -10
- package/src/lib/hooks/use-permissions.ts +3 -3
- package/src/lib/hooks/use-sorted-languages.ts +41 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/lib/load-i18n-messages.ts +4 -1
- package/src/lib/providers/channel-provider.tsx +11 -7
- package/src/lib/utils/config-utils.ts +19 -0
- package/src/lib/virtual.d.ts +3 -0
- package/LICENSE.md +0 -42
- package/src/app/routes/_authenticated/_facets/components/edit-facet-value.tsx +0 -129
- /package/src/{app/routes/_authenticated/_global-settings → lib}/utils/global-languages.ts +0 -0
|
@@ -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 =>
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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">{
|
|
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
|
-
*
|
|
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({
|
|
323
|
-
|
|
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
|
-
{
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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' : '', '
|
|
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';
|
|
@@ -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,
|
|
@@ -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: (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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[]>;
|
package/src/lib/graphql/api.ts
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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;
|