@vendure/dashboard 3.4.3-master-202509190229 → 3.4.3-master-202509230228
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/vite/vite-plugin-config.js +1 -0
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_administrators/administrators.tsx +1 -2
- package/src/app/routes/_authenticated/_assets/assets.graphql.ts +39 -0
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +18 -7
- package/src/app/routes/_authenticated/_assets/components/asset-tag-filter.tsx +206 -0
- package/src/app/routes/_authenticated/_assets/components/asset-tags-editor.tsx +226 -0
- package/src/app/routes/_authenticated/_assets/components/manage-tags-dialog.tsx +217 -0
- package/src/app/routes/_authenticated/_channels/channels.tsx +1 -2
- package/src/app/routes/_authenticated/_collections/collections.tsx +2 -16
- package/src/app/routes/_authenticated/_countries/countries.graphql.ts +2 -0
- package/src/app/routes/_authenticated/_countries/countries.tsx +1 -2
- package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +1 -2
- package/src/app/routes/_authenticated/_customers/customers.tsx +1 -2
- package/src/app/routes/_authenticated/_facets/facets.tsx +0 -1
- package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +302 -0
- package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +16 -0
- package/src/app/routes/_authenticated/_orders/components/seller-orders-card.tsx +61 -0
- package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +17 -10
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +31 -0
- package/src/app/routes/_authenticated/_orders/orders_.$aggregateOrderId_.seller-orders.$sellerOrderId.tsx +50 -0
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +17 -290
- package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +7 -39
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +4 -26
- package/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx +129 -0
- package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +8 -0
- package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +1 -2
- package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +1 -2
- package/src/app/routes/_authenticated/_products/products.tsx +1 -2
- package/src/app/routes/_authenticated/_promotions/promotions.tsx +1 -2
- package/src/app/routes/_authenticated/_roles/components/permissions-table-grid.tsx +251 -0
- package/src/app/routes/_authenticated/_roles/roles.tsx +1 -2
- package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +5 -3
- package/src/app/routes/_authenticated/_sellers/sellers.tsx +1 -2
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +1 -2
- package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +1 -2
- package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +1 -2
- package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +1 -2
- package/src/app/routes/_authenticated/_zones/zones.tsx +1 -2
- package/src/lib/components/data-table/data-table-bulk-actions.tsx +5 -14
- package/src/lib/components/data-table/use-all-bulk-actions.ts +19 -0
- package/src/lib/components/data-table/use-generated-columns.tsx +12 -3
- package/src/lib/components/layout/nav-main.tsx +50 -25
- package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +1 -1
- package/src/lib/components/shared/asset/asset-gallery.tsx +83 -50
- package/src/lib/components/shared/detail-page-button.tsx +6 -4
- package/src/lib/components/shared/paginated-list-data-table.tsx +1 -0
- package/src/lib/components/shared/vendure-image.tsx +9 -1
- package/src/lib/framework/defaults.ts +24 -0
- package/src/lib/framework/extension-api/types/navigation.ts +8 -0
- package/src/lib/framework/layout-engine/page-layout.tsx +96 -9
- package/src/lib/framework/nav-menu/nav-menu-extensions.ts +26 -0
- package/src/lib/framework/page/list-page.tsx +7 -0
- package/src/lib/hooks/use-custom-field-config.ts +19 -2
- package/src/lib/index.ts +7 -1
- package/src/lib/providers/channel-provider.tsx +22 -6
- package/src/lib/providers/server-config.tsx +1 -0
- package/src/app/routes/_authenticated/_collections/components/move-single-collection.tsx +0 -33
- package/src/app/routes/_authenticated/_roles/components/permissions-grid.tsx +0 -120
- package/src/lib/components/shared/asset/focal-point-control.tsx +0 -57
|
@@ -8,7 +8,7 @@ import { createFileRoute, Link } from '@tanstack/react-router';
|
|
|
8
8
|
import { PlusIcon } from 'lucide-react';
|
|
9
9
|
import { DeleteZonesBulkAction } from './components/zone-bulk-actions.js';
|
|
10
10
|
import { ZoneCountriesSheet } from './components/zone-countries-sheet.js';
|
|
11
|
-
import {
|
|
11
|
+
import { zoneListQuery } from './zones.graphql.js';
|
|
12
12
|
|
|
13
13
|
export const Route = createFileRoute('/_authenticated/_zones/zones')({
|
|
14
14
|
component: ZoneListPage,
|
|
@@ -20,7 +20,6 @@ function ZoneListPage() {
|
|
|
20
20
|
<ListPage
|
|
21
21
|
pageId="zone-list"
|
|
22
22
|
listQuery={zoneListQuery}
|
|
23
|
-
deleteMutation={deleteZoneDocument}
|
|
24
23
|
route={Route}
|
|
25
24
|
title="Zones"
|
|
26
25
|
defaultVisibility={{
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
'use
|
|
2
|
-
|
|
1
|
+
import { useAllBulkActions } from '@/vdb/components/data-table/use-all-bulk-actions.js';
|
|
3
2
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
4
3
|
import {
|
|
5
4
|
DropdownMenu,
|
|
@@ -7,11 +6,8 @@ import {
|
|
|
7
6
|
DropdownMenuItem,
|
|
8
7
|
DropdownMenuTrigger,
|
|
9
8
|
} from '@/vdb/components/ui/dropdown-menu.js';
|
|
10
|
-
import { getBulkActions } from '@/vdb/framework/data-table/data-table-extensions.js';
|
|
11
9
|
import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
|
|
12
10
|
import { useFloatingBulkActions } from '@/vdb/hooks/use-floating-bulk-actions.js';
|
|
13
|
-
import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
|
|
14
|
-
import { usePage } from '@/vdb/hooks/use-page.js';
|
|
15
11
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
16
12
|
import { Table } from '@tanstack/react-table';
|
|
17
13
|
import { ChevronDown } from 'lucide-react';
|
|
@@ -26,9 +22,7 @@ export function DataTableBulkActions<TData>({
|
|
|
26
22
|
table,
|
|
27
23
|
bulkActions,
|
|
28
24
|
}: Readonly<DataTableBulkActionsProps<TData>>) {
|
|
29
|
-
const
|
|
30
|
-
const pageBlock = usePageBlock();
|
|
31
|
-
const blockId = pageBlock?.blockId;
|
|
25
|
+
const allBulkActions = useAllBulkActions(bulkActions);
|
|
32
26
|
|
|
33
27
|
// Cache to store selected items across page changes
|
|
34
28
|
const selectedItemsCache = useRef<Map<string, TData>>(new Map());
|
|
@@ -63,18 +57,15 @@ export function DataTableBulkActions<TData>({
|
|
|
63
57
|
if (!shouldShow) {
|
|
64
58
|
return null;
|
|
65
59
|
}
|
|
66
|
-
const extendedBulkActions = pageId ? getBulkActions(pageId, blockId) : [];
|
|
67
|
-
const allBulkActions = [...extendedBulkActions, ...(bulkActions ?? [])];
|
|
68
|
-
allBulkActions.sort((a, b) => (a.order ?? 10_000) - (b.order ?? 10_000));
|
|
69
60
|
|
|
70
61
|
return (
|
|
71
62
|
<div
|
|
72
63
|
className="flex items-center gap-4 px-8 py-2 animate-in fade-in duration-200 fixed transform -translate-x-1/2 bg-white shadow-2xl rounded-md border z-50"
|
|
73
|
-
style={{
|
|
74
|
-
height: 'auto',
|
|
64
|
+
style={{
|
|
65
|
+
height: 'auto',
|
|
75
66
|
maxHeight: '60px',
|
|
76
67
|
bottom: position.bottom,
|
|
77
|
-
left: position.left
|
|
68
|
+
left: position.left,
|
|
78
69
|
}}
|
|
79
70
|
>
|
|
80
71
|
<span className="text-sm text-muted-foreground">
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getBulkActions } from '@/vdb/framework/data-table/data-table-extensions.js';
|
|
2
|
+
import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
|
|
3
|
+
import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
|
|
4
|
+
import { usePage } from '@/vdb/hooks/use-page.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @description
|
|
8
|
+
* Augments the provided Bulk Actions with any user-defined actions for the current
|
|
9
|
+
* page & block, and returns all of the bulk actions sorted by the `order` property.
|
|
10
|
+
*/
|
|
11
|
+
export function useAllBulkActions(bulkActions: BulkAction[]): BulkAction[] {
|
|
12
|
+
const { pageId } = usePage();
|
|
13
|
+
const pageBlock = usePageBlock();
|
|
14
|
+
const blockId = pageBlock?.blockId;
|
|
15
|
+
const extendedBulkActions = pageId ? getBulkActions(pageId, blockId) : [];
|
|
16
|
+
const allBulkActions = [...extendedBulkActions, ...(bulkActions ?? [])];
|
|
17
|
+
allBulkActions.sort((a, b) => (a.order ?? 10_000) - (b.order ?? 10_000));
|
|
18
|
+
return allBulkActions;
|
|
19
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { useAllBulkActions } from '@/vdb/components/data-table/use-all-bulk-actions.js';
|
|
1
2
|
import { DisplayComponent } from '@/vdb/framework/component-registry/display-component.js';
|
|
2
3
|
import {
|
|
3
4
|
FieldInfo,
|
|
4
5
|
getOperationVariablesFields,
|
|
5
6
|
getTypeFieldInfo,
|
|
6
7
|
} from '@/vdb/framework/document-introspection/get-document-structure.js';
|
|
8
|
+
import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
|
|
7
9
|
import { api } from '@/vdb/graphql/api.js';
|
|
8
10
|
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
9
11
|
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
@@ -51,6 +53,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
|
|
|
51
53
|
fields,
|
|
52
54
|
customizeColumns,
|
|
53
55
|
rowActions,
|
|
56
|
+
bulkActions,
|
|
54
57
|
deleteMutation,
|
|
55
58
|
additionalColumns,
|
|
56
59
|
defaultColumnOrder,
|
|
@@ -62,6 +65,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
|
|
|
62
65
|
fields: FieldInfo[];
|
|
63
66
|
customizeColumns?: CustomizeColumnConfig<T>;
|
|
64
67
|
rowActions?: RowAction<PaginatedListItemFields<T>>[];
|
|
68
|
+
bulkActions?: BulkAction[];
|
|
65
69
|
deleteMutation?: TypedDocumentNode<any, any>;
|
|
66
70
|
additionalColumns?: AdditionalColumns<T>;
|
|
67
71
|
defaultColumnOrder?: Array<string | number | symbol>;
|
|
@@ -71,6 +75,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
|
|
|
71
75
|
enableSorting?: boolean;
|
|
72
76
|
}>) {
|
|
73
77
|
const columnHelper = createColumnHelper<PaginatedListItemFields<T>>();
|
|
78
|
+
const allBulkActions = useAllBulkActions(bulkActions ?? []);
|
|
74
79
|
|
|
75
80
|
const { columns, customFieldColumnNames } = useMemo(() => {
|
|
76
81
|
const columnConfigs: Array<{ fieldInfo: FieldInfo; isCustomField: boolean }> = [];
|
|
@@ -169,8 +174,8 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
|
|
|
169
174
|
finalColumns = [...orderedColumns, ...remainingColumns];
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
if (includeActionsColumn && (rowActions || deleteMutation)) {
|
|
173
|
-
const rowActionColumn = getRowActions(rowActions, deleteMutation);
|
|
177
|
+
if (includeActionsColumn && (rowActions || deleteMutation || bulkActions)) {
|
|
178
|
+
const rowActionColumn = getRowActions(rowActions, deleteMutation, allBulkActions);
|
|
174
179
|
if (rowActionColumn) {
|
|
175
180
|
finalColumns.push(rowActionColumn);
|
|
176
181
|
}
|
|
@@ -212,13 +217,14 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
|
|
|
212
217
|
function getRowActions(
|
|
213
218
|
rowActions?: RowAction<any>[],
|
|
214
219
|
deleteMutation?: TypedDocumentNode<any, any>,
|
|
220
|
+
bulkActions?: BulkAction[],
|
|
215
221
|
): AccessorKeyColumnDef<any> | undefined {
|
|
216
222
|
return {
|
|
217
223
|
id: 'actions',
|
|
218
224
|
accessorKey: 'actions',
|
|
219
225
|
header: () => <Trans>Actions</Trans>,
|
|
220
226
|
enableColumnFilter: false,
|
|
221
|
-
cell: ({ row }) => {
|
|
227
|
+
cell: ({ row, table }) => {
|
|
222
228
|
return (
|
|
223
229
|
<DropdownMenu>
|
|
224
230
|
<DropdownMenuTrigger asChild>
|
|
@@ -235,6 +241,9 @@ function getRowActions(
|
|
|
235
241
|
{action.label}
|
|
236
242
|
</DropdownMenuItem>
|
|
237
243
|
))}
|
|
244
|
+
{bulkActions?.map((action, index) => (
|
|
245
|
+
<action.component key={`bulk-action-${index}`} selection={[row]} table={table} />
|
|
246
|
+
))}
|
|
238
247
|
{deleteMutation && (
|
|
239
248
|
<DeleteMutationRowAction deleteMutation={deleteMutation} row={row} />
|
|
240
249
|
)}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
NavMenuSection,
|
|
15
15
|
NavMenuSectionPlacement,
|
|
16
16
|
} from '@/vdb/framework/nav-menu/nav-menu-extensions.js';
|
|
17
|
+
import { usePermissions } from '@/vdb/hooks/use-permissions.js';
|
|
17
18
|
import { Link, useRouter, useRouterState } from '@tanstack/react-router';
|
|
18
19
|
import { ChevronRight } from 'lucide-react';
|
|
19
20
|
import * as React from 'react';
|
|
@@ -39,6 +40,7 @@ function escapeRegexChars(str: string): string {
|
|
|
39
40
|
export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavMenuItem> }>) {
|
|
40
41
|
const router = useRouter();
|
|
41
42
|
const routerState = useRouterState();
|
|
43
|
+
const { hasPermissions } = usePermissions();
|
|
42
44
|
const currentPath = routerState.location.pathname;
|
|
43
45
|
const basePath = router.basepath || '';
|
|
44
46
|
|
|
@@ -46,16 +48,20 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
46
48
|
const isPathActive = React.useCallback(
|
|
47
49
|
(itemUrl: string) => {
|
|
48
50
|
// Remove basepath prefix from current path for comparison
|
|
49
|
-
const normalizedCurrentPath = basePath
|
|
50
|
-
|
|
51
|
+
const normalizedCurrentPath = basePath
|
|
52
|
+
? currentPath.replace(new RegExp(`^${escapeRegexChars(basePath)}`), '')
|
|
53
|
+
: currentPath;
|
|
54
|
+
|
|
51
55
|
// Ensure normalized path starts with /
|
|
52
|
-
const cleanPath = normalizedCurrentPath.startsWith('/')
|
|
53
|
-
|
|
56
|
+
const cleanPath = normalizedCurrentPath.startsWith('/')
|
|
57
|
+
? normalizedCurrentPath
|
|
58
|
+
: `/${normalizedCurrentPath}`;
|
|
59
|
+
|
|
54
60
|
// Special handling for root path
|
|
55
61
|
if (itemUrl === '/') {
|
|
56
62
|
return cleanPath === '/' || cleanPath === '';
|
|
57
63
|
}
|
|
58
|
-
|
|
64
|
+
|
|
59
65
|
// For other paths, check exact match or prefix match
|
|
60
66
|
return cleanPath === itemUrl || cleanPath.startsWith(`${itemUrl}/`);
|
|
61
67
|
},
|
|
@@ -97,6 +103,20 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
97
103
|
return activeTopSections;
|
|
98
104
|
});
|
|
99
105
|
|
|
106
|
+
// Helper to check if an item is allowed based on permissions
|
|
107
|
+
const isItemAllowed = React.useCallback(
|
|
108
|
+
(item: NavMenuItem) => {
|
|
109
|
+
if (!item.requiresPermission) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const permissions = Array.isArray(item.requiresPermission)
|
|
113
|
+
? item.requiresPermission
|
|
114
|
+
: [item.requiresPermission];
|
|
115
|
+
return hasPermissions(permissions);
|
|
116
|
+
},
|
|
117
|
+
[hasPermissions],
|
|
118
|
+
);
|
|
119
|
+
|
|
100
120
|
// Helper to build a sorted list of sections for a given placement, memoized for stability
|
|
101
121
|
const getSortedSections = React.useCallback(
|
|
102
122
|
(placement: NavMenuSectionPlacement) => {
|
|
@@ -104,13 +124,24 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
104
124
|
.filter(item => item.placement === placement)
|
|
105
125
|
.slice()
|
|
106
126
|
.sort(sortByOrder)
|
|
107
|
-
.map(section =>
|
|
108
|
-
'items' in section
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
.map(section => {
|
|
128
|
+
if ('items' in section) {
|
|
129
|
+
// Filter items based on permissions
|
|
130
|
+
const allowedItems = (section.items ?? []).filter(isItemAllowed).sort(sortByOrder);
|
|
131
|
+
return { ...section, items: allowedItems };
|
|
132
|
+
}
|
|
133
|
+
return section;
|
|
134
|
+
})
|
|
135
|
+
.filter(section => {
|
|
136
|
+
// Drop sections that have no items after permission filtering
|
|
137
|
+
if ('items' in section) {
|
|
138
|
+
return section.items && section.items.length > 0;
|
|
139
|
+
}
|
|
140
|
+
// For single items, check if they're allowed
|
|
141
|
+
return isItemAllowed(section as NavMenuItem);
|
|
142
|
+
});
|
|
112
143
|
},
|
|
113
|
-
[items],
|
|
144
|
+
[items, isItemAllowed],
|
|
114
145
|
);
|
|
115
146
|
|
|
116
147
|
const topSections = React.useMemo(() => getSortedSections('top'), [getSortedSections]);
|
|
@@ -154,11 +185,7 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
154
185
|
return (
|
|
155
186
|
<NavItemWrapper key={item.title} locationId={item.id} order={item.order} offset={true}>
|
|
156
187
|
<SidebarMenuItem>
|
|
157
|
-
<SidebarMenuButton
|
|
158
|
-
tooltip={item.title}
|
|
159
|
-
asChild
|
|
160
|
-
isActive={isPathActive(item.url)}
|
|
161
|
-
>
|
|
188
|
+
<SidebarMenuButton tooltip={item.title} asChild isActive={isPathActive(item.url)}>
|
|
162
189
|
<Link to={item.url}>
|
|
163
190
|
{item.icon && <item.icon />}
|
|
164
191
|
<span>{item.title}</span>
|
|
@@ -220,11 +247,7 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
220
247
|
return (
|
|
221
248
|
<NavItemWrapper key={item.title} locationId={item.id} order={item.order} offset={true}>
|
|
222
249
|
<SidebarMenuItem>
|
|
223
|
-
<SidebarMenuButton
|
|
224
|
-
tooltip={item.title}
|
|
225
|
-
asChild
|
|
226
|
-
isActive={isPathActive(item.url)}
|
|
227
|
-
>
|
|
250
|
+
<SidebarMenuButton tooltip={item.title} asChild isActive={isPathActive(item.url)}>
|
|
228
251
|
<Link to={item.url}>
|
|
229
252
|
{item.icon && <item.icon />}
|
|
230
253
|
<span>{item.title}</span>
|
|
@@ -287,10 +310,12 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
287
310
|
</SidebarGroup>
|
|
288
311
|
|
|
289
312
|
{/* Bottom sections - will be pushed to the bottom by CSS */}
|
|
290
|
-
|
|
291
|
-
<
|
|
292
|
-
|
|
293
|
-
|
|
313
|
+
{bottomSections.length ? (
|
|
314
|
+
<SidebarGroup className="mt-auto">
|
|
315
|
+
<SidebarGroupLabel>Administration</SidebarGroupLabel>
|
|
316
|
+
<SidebarMenu>{bottomSections.map(renderBottomSection)}</SidebarMenu>
|
|
317
|
+
</SidebarGroup>
|
|
318
|
+
) : null}
|
|
294
319
|
</>
|
|
295
320
|
);
|
|
296
321
|
}
|
|
@@ -23,6 +23,8 @@ import { useDebounce } from '@uidotdev/usehooks';
|
|
|
23
23
|
import { Loader2, Search, Upload, X } from 'lucide-react';
|
|
24
24
|
import { useCallback, useState } from 'react';
|
|
25
25
|
import { useDropzone } from 'react-dropzone';
|
|
26
|
+
import { tagListDocument } from '../../../../app/routes/_authenticated/_assets/assets.graphql.js';
|
|
27
|
+
import { AssetTagFilter } from '../../../../app/routes/_authenticated/_assets/components/asset-tag-filter.js';
|
|
26
28
|
import { DetailPageButton } from '../detail-page-button.js';
|
|
27
29
|
import { AssetBulkAction, AssetBulkActions } from './asset-bulk-actions.js';
|
|
28
30
|
|
|
@@ -74,7 +76,7 @@ export type Asset = AssetFragment;
|
|
|
74
76
|
/**
|
|
75
77
|
* @description
|
|
76
78
|
* Props for the {@link AssetGallery} component.
|
|
77
|
-
*
|
|
79
|
+
*
|
|
78
80
|
* @docsCategory components
|
|
79
81
|
* @docsPage AssetGallery
|
|
80
82
|
*/
|
|
@@ -134,16 +136,16 @@ export interface AssetGalleryProps {
|
|
|
134
136
|
/**
|
|
135
137
|
* @description
|
|
136
138
|
* A component for displaying a gallery of assets.
|
|
137
|
-
*
|
|
139
|
+
*
|
|
138
140
|
* @example
|
|
139
141
|
* ```tsx
|
|
140
142
|
* <AssetGallery
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
onSelect={handleAssetSelect}
|
|
144
|
+
multiSelect="manual"
|
|
145
|
+
initialSelectedAssets={initialSelectedAssets}
|
|
146
|
+
fixedHeight={false}
|
|
147
|
+
displayBulkActions={false}
|
|
148
|
+
/>
|
|
147
149
|
* ```
|
|
148
150
|
*
|
|
149
151
|
* @docsCategory components
|
|
@@ -169,9 +171,19 @@ export function AssetGallery({
|
|
|
169
171
|
const debouncedSearch = useDebounce(search, 500);
|
|
170
172
|
const [assetType, setAssetType] = useState<string>(AssetType.ALL);
|
|
171
173
|
const [selected, setSelected] = useState<Asset[]>(initialSelectedAssets || []);
|
|
174
|
+
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
|
172
175
|
const queryClient = useQueryClient();
|
|
173
176
|
|
|
174
|
-
const queryKey = ['AssetGallery', page, pageSize, debouncedSearch, assetType];
|
|
177
|
+
const queryKey = ['AssetGallery', page, pageSize, debouncedSearch, assetType, selectedTags];
|
|
178
|
+
|
|
179
|
+
// Query for available tags to check if we should show the filter
|
|
180
|
+
const { data: tagsData } = useQuery({
|
|
181
|
+
queryKey: ['tags-check'],
|
|
182
|
+
queryFn: () => api.query(tagListDocument, { options: { take: 1 } }),
|
|
183
|
+
staleTime: 1000 * 60 * 5,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const hasTags = (tagsData?.tags.items?.length || 0) > 0;
|
|
175
187
|
|
|
176
188
|
// Query for assets
|
|
177
189
|
const { data, isLoading, refetch } = useQuery({
|
|
@@ -187,14 +199,20 @@ export function AssetGallery({
|
|
|
187
199
|
filter.type = { eq: assetType };
|
|
188
200
|
}
|
|
189
201
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
const options: any = {
|
|
203
|
+
skip: (page - 1) * pageSize,
|
|
204
|
+
take: pageSize,
|
|
205
|
+
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
|
206
|
+
sort: { createdAt: 'DESC' },
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Add tag filtering if tags are provided
|
|
210
|
+
if (selectedTags && selectedTags.length > 0) {
|
|
211
|
+
options.tags = selectedTags;
|
|
212
|
+
options.tagsOperator = 'AND';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return api.query(getAssetListDocument, { options });
|
|
198
216
|
},
|
|
199
217
|
});
|
|
200
218
|
|
|
@@ -262,10 +280,17 @@ export function AssetGallery({
|
|
|
262
280
|
// Check if an asset is selected
|
|
263
281
|
const isSelected = (asset: Asset) => selected.some(a => a.id === asset.id);
|
|
264
282
|
|
|
283
|
+
// Handle tag changes
|
|
284
|
+
const handleTagsChange = (tags: string[]) => {
|
|
285
|
+
setSelectedTags(tags);
|
|
286
|
+
setPage(1); // Reset to page 1 when tags change
|
|
287
|
+
};
|
|
288
|
+
|
|
265
289
|
// Clear filters
|
|
266
290
|
const clearFilters = () => {
|
|
267
291
|
setSearch('');
|
|
268
292
|
setAssetType(AssetType.ALL);
|
|
293
|
+
setSelectedTags([]);
|
|
269
294
|
setPage(1);
|
|
270
295
|
};
|
|
271
296
|
|
|
@@ -294,40 +319,48 @@ export function AssetGallery({
|
|
|
294
319
|
return (
|
|
295
320
|
<div className={`relative flex flex-col w-full ${fixedHeight ? 'h-[600px]' : 'h-full'} ${className}`}>
|
|
296
321
|
{showHeader && (
|
|
297
|
-
<div className="
|
|
298
|
-
<div className="
|
|
299
|
-
<
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
322
|
+
<div className="space-y-4 mb-4 flex-shrink-0">
|
|
323
|
+
<div className="flex flex-col md:flex-row gap-2">
|
|
324
|
+
<div className="relative flex-grow flex items-center gap-2">
|
|
325
|
+
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
326
|
+
<Input
|
|
327
|
+
placeholder="Search assets..."
|
|
328
|
+
value={search}
|
|
329
|
+
onChange={e => setSearch(e.target.value)}
|
|
330
|
+
className="pl-8"
|
|
331
|
+
/>
|
|
332
|
+
{(search || assetType !== AssetType.ALL || selectedTags.length > 0) && (
|
|
333
|
+
<Button
|
|
334
|
+
variant="ghost"
|
|
335
|
+
size="sm"
|
|
336
|
+
onClick={clearFilters}
|
|
337
|
+
className="absolute right-0"
|
|
338
|
+
>
|
|
339
|
+
<X className="h-4 w-4 mr-1" /> Clear filters
|
|
340
|
+
</Button>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
<Select value={assetType} onValueChange={setAssetType}>
|
|
344
|
+
<SelectTrigger className="w-full md:w-[180px]">
|
|
345
|
+
<SelectValue placeholder="Asset type" />
|
|
346
|
+
</SelectTrigger>
|
|
347
|
+
<SelectContent>
|
|
348
|
+
<SelectItem value={AssetType.ALL}>All types</SelectItem>
|
|
349
|
+
<SelectItem value={AssetType.IMAGE}>Images</SelectItem>
|
|
350
|
+
<SelectItem value={AssetType.VIDEO}>Video</SelectItem>
|
|
351
|
+
<SelectItem value={AssetType.BINARY}>Binary</SelectItem>
|
|
352
|
+
</SelectContent>
|
|
353
|
+
</Select>
|
|
354
|
+
<Button onClick={openFileDialog} className="whitespace-nowrap">
|
|
355
|
+
<Upload className="h-4 w-4 mr-2" /> <Trans>Upload</Trans>
|
|
356
|
+
</Button>
|
|
316
357
|
</div>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
<SelectItem value={AssetType.IMAGE}>Images</SelectItem>
|
|
324
|
-
<SelectItem value={AssetType.VIDEO}>Video</SelectItem>
|
|
325
|
-
<SelectItem value={AssetType.BINARY}>Binary</SelectItem>
|
|
326
|
-
</SelectContent>
|
|
327
|
-
</Select>
|
|
328
|
-
<Button onClick={openFileDialog} className="whitespace-nowrap">
|
|
329
|
-
<Upload className="h-4 w-4 mr-2" /> <Trans>Upload</Trans>
|
|
330
|
-
</Button>
|
|
358
|
+
|
|
359
|
+
{hasTags && (
|
|
360
|
+
<div className="flex items-center -mt-2">
|
|
361
|
+
<AssetTagFilter selectedTags={selectedTags} onTagsChange={handleTagsChange} />
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
331
364
|
</div>
|
|
332
365
|
)}
|
|
333
366
|
|
|
@@ -13,7 +13,7 @@ import { Button } from '../ui/button.js';
|
|
|
13
13
|
* ```tsx
|
|
14
14
|
* // Basic usage with ID (relative navigation)
|
|
15
15
|
* <DetailPageButton id="123" label="Product Name" />
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
17
|
*
|
|
18
18
|
* @example
|
|
19
19
|
* ```tsx
|
|
@@ -36,18 +36,20 @@ export function DetailPageButton({
|
|
|
36
36
|
label,
|
|
37
37
|
disabled,
|
|
38
38
|
search,
|
|
39
|
-
|
|
39
|
+
className,
|
|
40
|
+
}: Readonly<{
|
|
40
41
|
label: string | React.ReactNode;
|
|
41
42
|
id?: string;
|
|
42
43
|
href?: string;
|
|
43
44
|
disabled?: boolean;
|
|
44
45
|
search?: Record<string, string>;
|
|
45
|
-
|
|
46
|
+
className?: string;
|
|
47
|
+
}>) {
|
|
46
48
|
if (!id && !href) {
|
|
47
49
|
return <span>{label}</span>;
|
|
48
50
|
}
|
|
49
51
|
return (
|
|
50
|
-
<Button asChild variant="ghost" disabled={disabled}>
|
|
52
|
+
<Button asChild variant="ghost" disabled={disabled} className={className}>
|
|
51
53
|
<Link to={href ?? `./${id}`} search={search ?? {}} preload={false}>
|
|
52
54
|
{label}
|
|
53
55
|
{!disabled && <ChevronRight className="h-3 w-3 text-muted-foreground" />}
|
|
@@ -25,7 +25,7 @@ export interface AssetLike {
|
|
|
25
25
|
* @docsPage VendureImage
|
|
26
26
|
* @since 3.4.0
|
|
27
27
|
*/
|
|
28
|
-
export type ImagePreset = 'tiny' | 'thumb' | 'small' | 'medium' | 'large' | null;
|
|
28
|
+
export type ImagePreset = 'tiny' | 'thumb' | 'small' | 'medium' | 'large' | 'full' | null;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* @description
|
|
@@ -219,6 +219,10 @@ function getMinDimensions(preset?: ImagePreset, width?: number, height?: number)
|
|
|
219
219
|
return { width: 300, height: 300 };
|
|
220
220
|
case 'medium':
|
|
221
221
|
return { width: 500, height: 500 };
|
|
222
|
+
case 'large':
|
|
223
|
+
return { width: 800, height: 800 };
|
|
224
|
+
case 'full':
|
|
225
|
+
return { width: undefined, height: undefined };
|
|
222
226
|
}
|
|
223
227
|
}
|
|
224
228
|
|
|
@@ -258,6 +262,10 @@ export function PlaceholderImage({
|
|
|
258
262
|
width = 800;
|
|
259
263
|
height = 800;
|
|
260
264
|
break;
|
|
265
|
+
case 'full':
|
|
266
|
+
width = 1200;
|
|
267
|
+
height = 1200;
|
|
268
|
+
break;
|
|
261
269
|
default:
|
|
262
270
|
break;
|
|
263
271
|
}
|