@vendure/dashboard 3.3.6-master-202507011151 → 3.3.6-master-202507020959
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/package.json +4 -4
- package/src/app/common/delete-bulk-action.tsx +147 -0
- package/src/app/common/duplicate-bulk-action.tsx +1 -1
- package/src/app/routes/_authenticated/_administrators/administrators.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_administrators/administrators.tsx +7 -0
- package/src/app/routes/_authenticated/_administrators/components/administrator-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_assets/assets.graphql.ts +11 -0
- package/src/app/routes/_authenticated/_assets/assets.tsx +10 -2
- package/src/app/routes/_authenticated/_assets/components/asset-bulk-actions.tsx +45 -0
- package/src/app/routes/_authenticated/_channels/channels.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_channels/channels.tsx +7 -0
- package/src/app/routes/_authenticated/_channels/components/channel-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +39 -110
- package/src/app/routes/_authenticated/_countries/components/country-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_countries/countries.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_countries/countries.tsx +7 -0
- package/src/app/routes/_authenticated/_customer-groups/components/customer-group-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_customer-groups/customer-groups.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +7 -0
- package/src/app/routes/_authenticated/_customers/components/customer-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_customers/customers.graphql.ts +9 -1
- package/src/app/routes/_authenticated/_customers/customers.tsx +7 -0
- package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +104 -0
- package/src/app/routes/_authenticated/_facets/facets.graphql.ts +30 -0
- package/src/app/routes/_authenticated/_facets/facets.tsx +24 -0
- package/src/app/routes/_authenticated/_payment-methods/components/payment-method-bulk-actions.tsx +58 -0
- package/src/app/routes/_authenticated/_payment-methods/payment-methods.graphql.ts +27 -0
- package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +30 -8
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +4 -1
- package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +36 -110
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +36 -105
- package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +82 -0
- package/src/app/routes/_authenticated/_promotions/promotions.graphql.ts +25 -0
- package/src/app/routes/_authenticated/_promotions/promotions.tsx +24 -0
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_roles/components/role-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_roles/roles.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_roles/roles.tsx +7 -0
- package/src/app/routes/_authenticated/_sellers/components/seller-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_sellers/sellers.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_sellers/sellers.tsx +7 -0
- package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-bulk-actions.tsx +61 -0
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts +27 -0
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +19 -0
- package/src/app/routes/_authenticated/_stock-locations/components/stock-location-bulk-actions.tsx +58 -0
- package/src/app/routes/_authenticated/_stock-locations/stock-locations.graphql.ts +25 -0
- package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +19 -0
- package/src/app/routes/_authenticated/_tax-categories/components/tax-category-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_tax-categories/tax-categories.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +7 -0
- package/src/app/routes/_authenticated/_tax-rates/components/tax-rate-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_tax-rates/tax-rates.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +7 -0
- package/src/app/routes/_authenticated/_zones/components/zone-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_zones/zones.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_zones/zones.tsx +7 -0
- package/src/lib/components/shared/asset/asset-bulk-actions.tsx +90 -0
- package/src/lib/components/shared/asset/asset-gallery.tsx +12 -7
- package/src/lib/components/shared/assign-to-channel-bulk-action.tsx +70 -0
- package/src/{app/routes/_authenticated/_products/components → lib/components/shared}/assign-to-channel-dialog.tsx +48 -30
- package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +89 -0
- package/src/lib/framework/component-registry/component-registry.tsx +31 -47
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +29 -95
- package/src/lib/framework/extension-api/display-component-extensions.tsx +69 -0
- package/src/lib/framework/extension-api/extension-api-types.ts +18 -160
- package/src/lib/framework/extension-api/input-component-extensions.tsx +69 -0
- package/src/lib/framework/extension-api/logic/alerts.ts +10 -0
- package/src/lib/framework/extension-api/logic/data-table.ts +60 -0
- package/src/lib/framework/extension-api/logic/detail-forms.ts +48 -0
- package/src/lib/framework/extension-api/logic/form-components.ts +13 -0
- package/src/lib/framework/extension-api/logic/index.ts +8 -0
- package/src/lib/framework/extension-api/logic/layout.ts +22 -0
- package/src/lib/framework/extension-api/logic/navigation.ts +37 -0
- package/src/lib/framework/extension-api/logic/widgets.ts +10 -0
- package/src/lib/framework/extension-api/types/alerts.ts +54 -0
- package/src/lib/framework/extension-api/types/data-table.ts +64 -0
- package/src/lib/framework/extension-api/types/detail-forms.ts +81 -0
- package/src/lib/framework/extension-api/types/form-components.ts +32 -0
- package/src/lib/framework/extension-api/types/index.ts +8 -0
- package/src/lib/framework/extension-api/types/layout.ts +78 -0
- package/src/lib/framework/extension-api/types/navigation.ts +19 -0
- package/src/lib/framework/extension-api/types/widgets.ts +94 -0
- package/src/lib/framework/page/detail-page.tsx +48 -3
- package/src/lib/framework/registry/registry-types.ts +3 -0
- package/src/app/routes/_authenticated/_collections/components/assign-collections-to-channel-dialog.tsx +0 -110
|
@@ -6,6 +6,11 @@ import { ListPage } from '@/framework/page/list-page.js';
|
|
|
6
6
|
import { Trans } from '@/lib/trans.js';
|
|
7
7
|
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
8
8
|
import { PlusIcon } from 'lucide-react';
|
|
9
|
+
import {
|
|
10
|
+
AssignStockLocationsToChannelBulkAction,
|
|
11
|
+
DeleteStockLocationsBulkAction,
|
|
12
|
+
RemoveStockLocationsFromChannelBulkAction,
|
|
13
|
+
} from './components/stock-location-bulk-actions.js';
|
|
9
14
|
import { deleteStockLocationDocument, stockLocationListQuery } from './stock-locations.graphql.js';
|
|
10
15
|
|
|
11
16
|
export const Route = createFileRoute('/_authenticated/_stock-locations/stock-locations')({
|
|
@@ -32,6 +37,20 @@ function StockLocationListPage() {
|
|
|
32
37
|
name: { contains: searchTerm },
|
|
33
38
|
};
|
|
34
39
|
}}
|
|
40
|
+
bulkActions={[
|
|
41
|
+
{
|
|
42
|
+
component: AssignStockLocationsToChannelBulkAction,
|
|
43
|
+
order: 100,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
component: RemoveStockLocationsFromChannelBulkAction,
|
|
47
|
+
order: 200,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
component: DeleteStockLocationsBulkAction,
|
|
51
|
+
order: 500,
|
|
52
|
+
},
|
|
53
|
+
]}
|
|
35
54
|
>
|
|
36
55
|
<PageActionBarRight>
|
|
37
56
|
<PermissionGuard requires={['CreateStockLocation']}>
|
package/src/app/routes/_authenticated/_tax-categories/components/tax-category-bulk-actions.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BulkActionComponent } from '@/framework/data-table/data-table-types.js';
|
|
2
|
+
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
3
|
+
import { deleteTaxCategoriesDocument } from '../tax-categories.graphql.js';
|
|
4
|
+
|
|
5
|
+
export const DeleteTaxCategoriesBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
|
|
6
|
+
return (
|
|
7
|
+
<DeleteBulkAction
|
|
8
|
+
mutationDocument={deleteTaxCategoriesDocument}
|
|
9
|
+
entityName="tax categories"
|
|
10
|
+
requiredPermissions={['DeleteTaxCategory']}
|
|
11
|
+
selection={selection}
|
|
12
|
+
table={table}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
@@ -7,6 +7,7 @@ import { ListPage } from '@/framework/page/list-page.js';
|
|
|
7
7
|
import { Trans } from '@/lib/trans.js';
|
|
8
8
|
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
9
9
|
import { PlusIcon } from 'lucide-react';
|
|
10
|
+
import { DeleteTaxCategoriesBulkAction } from './components/tax-category-bulk-actions.js';
|
|
10
11
|
import { deleteTaxCategoryDocument, taxCategoryListQuery } from './tax-categories.graphql.js';
|
|
11
12
|
|
|
12
13
|
export const Route = createFileRoute('/_authenticated/_tax-categories/tax-categories')({
|
|
@@ -49,6 +50,12 @@ function TaxCategoryListPage() {
|
|
|
49
50
|
),
|
|
50
51
|
},
|
|
51
52
|
}}
|
|
53
|
+
bulkActions={[
|
|
54
|
+
{
|
|
55
|
+
component: DeleteTaxCategoriesBulkAction,
|
|
56
|
+
order: 500,
|
|
57
|
+
},
|
|
58
|
+
]}
|
|
52
59
|
>
|
|
53
60
|
<PageActionBarRight>
|
|
54
61
|
<PermissionGuard requires={['CreateTaxCategory']}>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BulkActionComponent } from '@/framework/data-table/data-table-types.js';
|
|
2
|
+
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
3
|
+
import { deleteTaxRatesDocument } from '../tax-rates.graphql.js';
|
|
4
|
+
|
|
5
|
+
export const DeleteTaxRatesBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
|
|
6
|
+
return (
|
|
7
|
+
<DeleteBulkAction
|
|
8
|
+
mutationDocument={deleteTaxRatesDocument}
|
|
9
|
+
entityName="tax rates"
|
|
10
|
+
requiredPermissions={['DeleteTaxRate']}
|
|
11
|
+
selection={selection}
|
|
12
|
+
table={table}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
@@ -10,6 +10,7 @@ import { createFileRoute, Link } from '@tanstack/react-router';
|
|
|
10
10
|
import { PlusIcon } from 'lucide-react';
|
|
11
11
|
import { taxCategoryListQuery } from '../_tax-categories/tax-categories.graphql.js';
|
|
12
12
|
import { zoneListQuery } from '../_zones/zones.graphql.js';
|
|
13
|
+
import { DeleteTaxRatesBulkAction } from './components/tax-rate-bulk-actions.js';
|
|
13
14
|
import { deleteTaxRateDocument, taxRateListQuery } from './tax-rates.graphql.js';
|
|
14
15
|
|
|
15
16
|
export const Route = createFileRoute('/_authenticated/_tax-rates/tax-rates')({
|
|
@@ -92,6 +93,12 @@ function TaxRateListPage() {
|
|
|
92
93
|
cell: ({ row }) => `${row.original.value}%`,
|
|
93
94
|
},
|
|
94
95
|
}}
|
|
96
|
+
bulkActions={[
|
|
97
|
+
{
|
|
98
|
+
component: DeleteTaxRatesBulkAction,
|
|
99
|
+
order: 500,
|
|
100
|
+
},
|
|
101
|
+
]}
|
|
95
102
|
>
|
|
96
103
|
<PageActionBarRight>
|
|
97
104
|
<PermissionGuard requires={['CreateTaxRate']}>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BulkActionComponent } from '@/framework/data-table/data-table-types.js';
|
|
2
|
+
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
3
|
+
import { deleteZonesDocument } from '../zones.graphql.js';
|
|
4
|
+
|
|
5
|
+
export const DeleteZonesBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
|
|
6
|
+
return (
|
|
7
|
+
<DeleteBulkAction
|
|
8
|
+
mutationDocument={deleteZonesDocument}
|
|
9
|
+
entityName="zones"
|
|
10
|
+
requiredPermissions={['DeleteZone']}
|
|
11
|
+
selection={selection}
|
|
12
|
+
table={table}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
@@ -6,6 +6,7 @@ import { ListPage } from '@/framework/page/list-page.js';
|
|
|
6
6
|
import { Trans } from '@/lib/trans.js';
|
|
7
7
|
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
8
8
|
import { PlusIcon } from 'lucide-react';
|
|
9
|
+
import { DeleteZonesBulkAction } from './components/zone-bulk-actions.js';
|
|
9
10
|
import { ZoneCountriesSheet } from './components/zone-countries-sheet.js';
|
|
10
11
|
import { deleteZoneDocument, zoneListQuery } from './zones.graphql.js';
|
|
11
12
|
|
|
@@ -41,6 +42,12 @@ function ZoneListPage() {
|
|
|
41
42
|
),
|
|
42
43
|
},
|
|
43
44
|
}}
|
|
45
|
+
bulkActions={[
|
|
46
|
+
{
|
|
47
|
+
component: DeleteZonesBulkAction,
|
|
48
|
+
order: 500,
|
|
49
|
+
},
|
|
50
|
+
]}
|
|
44
51
|
>
|
|
45
52
|
<PageActionBarRight>
|
|
46
53
|
<PermissionGuard requires={['CreateZone']}>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button.js';
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuTrigger,
|
|
9
|
+
} from '@/components/ui/dropdown-menu.js';
|
|
10
|
+
import { getBulkActions } from '@/framework/data-table/data-table-extensions.js';
|
|
11
|
+
import { usePageBlock } from '@/hooks/use-page-block.js';
|
|
12
|
+
import { usePage } from '@/hooks/use-page.js';
|
|
13
|
+
import { Trans } from '@/lib/trans.js';
|
|
14
|
+
import { ChevronDown } from 'lucide-react';
|
|
15
|
+
import { Asset } from './asset-gallery.js';
|
|
16
|
+
|
|
17
|
+
export type AssetBulkActionContext = {
|
|
18
|
+
selection: Asset[];
|
|
19
|
+
refetch: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type AssetBulkActionComponent = React.FunctionComponent<AssetBulkActionContext>;
|
|
23
|
+
|
|
24
|
+
export type AssetBulkAction = {
|
|
25
|
+
order?: number;
|
|
26
|
+
component: AssetBulkActionComponent;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface AssetBulkActionsProps {
|
|
30
|
+
selection: Asset[];
|
|
31
|
+
bulkActions?: AssetBulkAction[];
|
|
32
|
+
refetch: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function AssetBulkActions({ selection, bulkActions, refetch }: AssetBulkActionsProps) {
|
|
36
|
+
const { pageId } = usePage();
|
|
37
|
+
const { blockId } = usePageBlock();
|
|
38
|
+
|
|
39
|
+
if (selection.length === 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Get extended bulk actions from the registry
|
|
44
|
+
const extendedBulkActions = pageId ? getBulkActions(pageId, blockId) : [];
|
|
45
|
+
|
|
46
|
+
// Convert DataTable bulk actions to Asset bulk actions
|
|
47
|
+
const convertedBulkActions: AssetBulkAction[] = extendedBulkActions.map(action => ({
|
|
48
|
+
order: action.order,
|
|
49
|
+
component: ({ selection }) => {
|
|
50
|
+
// Create a mock table context for compatibility
|
|
51
|
+
const mockTable = {
|
|
52
|
+
getState: () => ({ rowSelection: {} }),
|
|
53
|
+
getRow: () => null,
|
|
54
|
+
} as any;
|
|
55
|
+
|
|
56
|
+
const ActionComponent = action.component;
|
|
57
|
+
return <ActionComponent selection={selection} table={mockTable} />;
|
|
58
|
+
},
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
const allBulkActions = [...convertedBulkActions, ...(bulkActions ?? [])];
|
|
62
|
+
allBulkActions.sort((a, b) => (a.order ?? 10_000) - (b.order ?? 10_000));
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex items-center gap-2 px-2 py-1 mb-2 bg-muted/50 rounded-md border">
|
|
66
|
+
<span className="text-sm text-muted-foreground">
|
|
67
|
+
<Trans>{selection.length} selected</Trans>
|
|
68
|
+
</span>
|
|
69
|
+
<DropdownMenu>
|
|
70
|
+
<DropdownMenuTrigger asChild>
|
|
71
|
+
<Button variant="outline" size="sm" className="h-8">
|
|
72
|
+
<Trans>With selected...</Trans>
|
|
73
|
+
<ChevronDown className="ml-2 h-4 w-4" />
|
|
74
|
+
</Button>
|
|
75
|
+
</DropdownMenuTrigger>
|
|
76
|
+
<DropdownMenuContent align="start">
|
|
77
|
+
{allBulkActions.length > 0 ? (
|
|
78
|
+
allBulkActions.map((action, index) => (
|
|
79
|
+
<action.component key={`asset-bulk-action-${index}`} selection={selection} refetch={refetch} />
|
|
80
|
+
))
|
|
81
|
+
) : (
|
|
82
|
+
<DropdownMenuItem className="text-muted-foreground" disabled>
|
|
83
|
+
<Trans>No actions available</Trans>
|
|
84
|
+
</DropdownMenuItem>
|
|
85
|
+
)}
|
|
86
|
+
</DropdownMenuContent>
|
|
87
|
+
</DropdownMenu>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -16,14 +16,15 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|
|
16
16
|
import { api } from '@/graphql/api.js';
|
|
17
17
|
import { assetFragment, AssetFragment } from '@/graphql/fragments.js';
|
|
18
18
|
import { graphql } from '@/graphql/graphql.js';
|
|
19
|
-
import { formatFileSize } from '@/lib/utils.js';
|
|
20
19
|
import { Trans } from '@/lib/trans.js';
|
|
20
|
+
import { formatFileSize } from '@/lib/utils.js';
|
|
21
21
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
22
|
+
import { useDebounce } from '@uidotdev/usehooks';
|
|
22
23
|
import { Loader2, Search, Upload, X } from 'lucide-react';
|
|
23
24
|
import { useCallback, useState } from 'react';
|
|
24
25
|
import { useDropzone } from 'react-dropzone';
|
|
25
|
-
import { useDebounce } from '@uidotdev/usehooks';
|
|
26
26
|
import { DetailPageButton } from '../detail-page-button.js';
|
|
27
|
+
import { AssetBulkAction, AssetBulkActions } from './asset-bulk-actions.js';
|
|
27
28
|
|
|
28
29
|
const getAssetListDocument = graphql(
|
|
29
30
|
`
|
|
@@ -76,7 +77,7 @@ export interface AssetGalleryProps {
|
|
|
76
77
|
/**
|
|
77
78
|
* @description
|
|
78
79
|
* Defines whether multiple assets can be selected.
|
|
79
|
-
*
|
|
80
|
+
*
|
|
80
81
|
* If set to 'auto', the asset selection will be toggled when the user clicks on an asset.
|
|
81
82
|
* If set to 'manual', multiple selection will occur only if the user holds down the control/cmd key.
|
|
82
83
|
*/
|
|
@@ -87,6 +88,7 @@ export interface AssetGalleryProps {
|
|
|
87
88
|
showHeader?: boolean;
|
|
88
89
|
className?: string;
|
|
89
90
|
onFilesDropped?: (files: File[]) => void;
|
|
91
|
+
bulkActions?: AssetBulkAction[];
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
export function AssetGallery({
|
|
@@ -99,6 +101,7 @@ export function AssetGallery({
|
|
|
99
101
|
showHeader = true,
|
|
100
102
|
className = '',
|
|
101
103
|
onFilesDropped,
|
|
104
|
+
bulkActions,
|
|
102
105
|
}: AssetGalleryProps) {
|
|
103
106
|
// State
|
|
104
107
|
const [page, setPage] = useState(1);
|
|
@@ -111,7 +114,7 @@ export function AssetGallery({
|
|
|
111
114
|
const queryKey = ['AssetGallery', page, pageSize, debouncedSearch, assetType];
|
|
112
115
|
|
|
113
116
|
// Query for assets
|
|
114
|
-
const { data, isLoading } = useQuery({
|
|
117
|
+
const { data, isLoading, refetch } = useQuery({
|
|
115
118
|
queryKey,
|
|
116
119
|
queryFn: () => {
|
|
117
120
|
const filter: Record<string, any> = {};
|
|
@@ -173,7 +176,6 @@ export function AssetGallery({
|
|
|
173
176
|
return;
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
|
|
177
179
|
// Manual mode - check for modifier key
|
|
178
180
|
const isModifierKeyPressed = event.metaKey || event.ctrlKey;
|
|
179
181
|
|
|
@@ -269,6 +271,9 @@ export function AssetGallery({
|
|
|
269
271
|
</div>
|
|
270
272
|
)}
|
|
271
273
|
|
|
274
|
+
{/* Bulk actions bar */}
|
|
275
|
+
<AssetBulkActions selection={selected} bulkActions={bulkActions} refetch={refetch} />
|
|
276
|
+
|
|
272
277
|
<div
|
|
273
278
|
{...getRootProps()}
|
|
274
279
|
className={`
|
|
@@ -300,7 +305,7 @@ export function AssetGallery({
|
|
|
300
305
|
${isSelected(asset as Asset) ? 'ring-2 ring-primary' : ''}
|
|
301
306
|
flex flex-col min-w-[120px]
|
|
302
307
|
`}
|
|
303
|
-
onClick={
|
|
308
|
+
onClick={e => handleSelect(asset as Asset, e)}
|
|
304
309
|
>
|
|
305
310
|
<div
|
|
306
311
|
className="relative w-full bg-muted/30"
|
|
@@ -324,7 +329,7 @@ export function AssetGallery({
|
|
|
324
329
|
<p className="text-xs line-clamp-2 min-h-[2.5rem]" title={asset.name}>
|
|
325
330
|
{asset.name}
|
|
326
331
|
</p>
|
|
327
|
-
<div className=
|
|
332
|
+
<div className="flex justify-between items-center">
|
|
328
333
|
{asset.fileSize && (
|
|
329
334
|
<p className="text-xs text-muted-foreground mt-1">
|
|
330
335
|
{formatFileSize(asset.fileSize)}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { LayersIcon } from 'lucide-react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { DataTableBulkActionItem } from '@/components/data-table/data-table-bulk-action-item.js';
|
|
5
|
+
import { AssignToChannelDialog } from '@/components/shared/assign-to-channel-dialog.js';
|
|
6
|
+
import { useChannel, usePaginatedList } from '@/index.js';
|
|
7
|
+
import { Trans } from '@/lib/trans.js';
|
|
8
|
+
|
|
9
|
+
interface AssignToChannelBulkActionProps {
|
|
10
|
+
selection: any[];
|
|
11
|
+
table: any;
|
|
12
|
+
entityType: string;
|
|
13
|
+
mutationFn: (variables: any) => Promise<any>;
|
|
14
|
+
requiredPermissions: string[];
|
|
15
|
+
buildInput: (channelId: string, additionalData?: Record<string, any>) => Record<string, any>;
|
|
16
|
+
additionalFields?: React.ReactNode;
|
|
17
|
+
additionalData?: Record<string, any>;
|
|
18
|
+
/**
|
|
19
|
+
* Additional callback to run on success, after the standard refetch and reset
|
|
20
|
+
*/
|
|
21
|
+
onSuccess?: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function AssignToChannelBulkAction({
|
|
25
|
+
selection,
|
|
26
|
+
table,
|
|
27
|
+
entityType,
|
|
28
|
+
mutationFn,
|
|
29
|
+
requiredPermissions,
|
|
30
|
+
buildInput,
|
|
31
|
+
additionalFields,
|
|
32
|
+
additionalData = {},
|
|
33
|
+
onSuccess,
|
|
34
|
+
}: Readonly<AssignToChannelBulkActionProps>) {
|
|
35
|
+
const { refetchPaginatedList } = usePaginatedList();
|
|
36
|
+
const { channels } = useChannel();
|
|
37
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
38
|
+
|
|
39
|
+
if (channels.length < 2) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const handleSuccess = () => {
|
|
44
|
+
refetchPaginatedList();
|
|
45
|
+
table.resetRowSelection();
|
|
46
|
+
onSuccess?.();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<DataTableBulkActionItem
|
|
52
|
+
requiresPermission={requiredPermissions}
|
|
53
|
+
onClick={() => setDialogOpen(true)}
|
|
54
|
+
label={<Trans>Assign to channel</Trans>}
|
|
55
|
+
icon={LayersIcon}
|
|
56
|
+
/>
|
|
57
|
+
<AssignToChannelDialog
|
|
58
|
+
open={dialogOpen}
|
|
59
|
+
onOpenChange={setDialogOpen}
|
|
60
|
+
entityIds={selection.map(s => s.id)}
|
|
61
|
+
entityType={entityType}
|
|
62
|
+
mutationFn={mutationFn}
|
|
63
|
+
onSuccess={handleSuccess}
|
|
64
|
+
buildInput={buildInput}
|
|
65
|
+
additionalFields={additionalFields}
|
|
66
|
+
additionalData={additionalData}
|
|
67
|
+
/>
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMutation } from '@tanstack/react-query';
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { ReactNode, useState } from 'react';
|
|
3
3
|
import { toast } from 'sonner';
|
|
4
4
|
|
|
5
5
|
import { ChannelCodeLabel } from '@/components/shared/channel-code-label.js';
|
|
@@ -23,9 +23,24 @@ interface AssignToChannelDialogProps {
|
|
|
23
23
|
open: boolean;
|
|
24
24
|
onOpenChange: (open: boolean) => void;
|
|
25
25
|
entityIds: string[];
|
|
26
|
-
entityType:
|
|
26
|
+
entityType: string;
|
|
27
27
|
mutationFn: (variables: any) => Promise<ResultOf<any>>;
|
|
28
28
|
onSuccess?: () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Function to build the input object for the mutation
|
|
31
|
+
* @param channelId - The selected channel ID
|
|
32
|
+
* @param additionalData - Any additional data (like priceFactor for products)
|
|
33
|
+
* @returns The input object for the mutation
|
|
34
|
+
*/
|
|
35
|
+
buildInput: (channelId: string, additionalData?: Record<string, any>) => Record<string, any>;
|
|
36
|
+
/**
|
|
37
|
+
* Optional additional form fields to render
|
|
38
|
+
*/
|
|
39
|
+
additionalFields?: ReactNode;
|
|
40
|
+
/**
|
|
41
|
+
* Optional additional data to pass to buildInput
|
|
42
|
+
*/
|
|
43
|
+
additionalData?: Record<string, any>;
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
export function AssignToChannelDialog({
|
|
@@ -35,10 +50,12 @@ export function AssignToChannelDialog({
|
|
|
35
50
|
entityType,
|
|
36
51
|
mutationFn,
|
|
37
52
|
onSuccess,
|
|
38
|
-
|
|
53
|
+
buildInput,
|
|
54
|
+
additionalFields,
|
|
55
|
+
additionalData = {},
|
|
56
|
+
}: Readonly<AssignToChannelDialogProps>) {
|
|
39
57
|
const { i18n } = useLingui();
|
|
40
58
|
const [selectedChannelId, setSelectedChannelId] = useState<string>('');
|
|
41
|
-
const [priceFactor, setPriceFactor] = useState<number>(1);
|
|
42
59
|
const { channels, selectedChannel } = useChannel();
|
|
43
60
|
|
|
44
61
|
// Filter out the currently selected channel from available options
|
|
@@ -62,19 +79,7 @@ export function AssignToChannelDialog({
|
|
|
62
79
|
return;
|
|
63
80
|
}
|
|
64
81
|
|
|
65
|
-
const input =
|
|
66
|
-
entityType === 'products'
|
|
67
|
-
? {
|
|
68
|
-
productIds: entityIds,
|
|
69
|
-
channelId: selectedChannelId,
|
|
70
|
-
priceFactor,
|
|
71
|
-
}
|
|
72
|
-
: {
|
|
73
|
-
productVariantIds: entityIds,
|
|
74
|
-
channelId: selectedChannelId,
|
|
75
|
-
priceFactor,
|
|
76
|
-
};
|
|
77
|
-
|
|
82
|
+
const input = buildInput(selectedChannelId, additionalData);
|
|
78
83
|
mutate({ input });
|
|
79
84
|
};
|
|
80
85
|
|
|
@@ -109,19 +114,7 @@ export function AssignToChannelDialog({
|
|
|
109
114
|
</SelectContent>
|
|
110
115
|
</Select>
|
|
111
116
|
</div>
|
|
112
|
-
|
|
113
|
-
<label className="text-sm font-medium">
|
|
114
|
-
<Trans>Price conversion factor</Trans>
|
|
115
|
-
</label>
|
|
116
|
-
<Input
|
|
117
|
-
type="number"
|
|
118
|
-
min="0"
|
|
119
|
-
max="99999"
|
|
120
|
-
step="0.01"
|
|
121
|
-
value={priceFactor}
|
|
122
|
-
onChange={e => setPriceFactor(parseFloat(e.target.value) || 1)}
|
|
123
|
-
/>
|
|
124
|
-
</div>
|
|
117
|
+
{additionalFields}
|
|
125
118
|
</div>
|
|
126
119
|
<DialogFooter>
|
|
127
120
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
@@ -135,3 +128,28 @@ export function AssignToChannelDialog({
|
|
|
135
128
|
</Dialog>
|
|
136
129
|
);
|
|
137
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Hook for managing price factor state in assign-to-channel dialogs
|
|
134
|
+
*/
|
|
135
|
+
export function usePriceFactor() {
|
|
136
|
+
const [priceFactor, setPriceFactor] = useState<number>(1);
|
|
137
|
+
|
|
138
|
+
const priceFactorField = (
|
|
139
|
+
<div className="grid gap-2">
|
|
140
|
+
<label className="text-sm font-medium">
|
|
141
|
+
<Trans>Price conversion factor</Trans>
|
|
142
|
+
</label>
|
|
143
|
+
<Input
|
|
144
|
+
type="number"
|
|
145
|
+
min="0"
|
|
146
|
+
max="99999"
|
|
147
|
+
step="0.01"
|
|
148
|
+
value={priceFactor}
|
|
149
|
+
onChange={e => setPriceFactor(parseFloat(e.target.value) || 1)}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return { priceFactor, priceFactorField };
|
|
155
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useMutation } from '@tanstack/react-query';
|
|
2
|
+
import { LayersIcon } from 'lucide-react';
|
|
3
|
+
import { toast } from 'sonner';
|
|
4
|
+
|
|
5
|
+
import { DataTableBulkActionItem } from '@/components/data-table/data-table-bulk-action-item.js';
|
|
6
|
+
import { ResultOf } from '@/graphql/graphql.js';
|
|
7
|
+
import { useChannel, usePaginatedList } from '@/index.js';
|
|
8
|
+
import { Trans, useLingui } from '@/lib/trans.js';
|
|
9
|
+
|
|
10
|
+
interface RemoveFromChannelBulkActionProps {
|
|
11
|
+
selection: any[];
|
|
12
|
+
table: any;
|
|
13
|
+
entityType: string;
|
|
14
|
+
mutationFn: (variables: any) => Promise<ResultOf<any>>;
|
|
15
|
+
requiredPermissions: string[];
|
|
16
|
+
buildInput: () => Record<string, any>;
|
|
17
|
+
/**
|
|
18
|
+
* Additional callback to run on success, after the standard refetch and reset
|
|
19
|
+
* @param result - The result from the mutation
|
|
20
|
+
*/
|
|
21
|
+
onSuccess?: (result?: ResultOf<any>) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Custom success message. If not provided, a default message will be used.
|
|
24
|
+
*/
|
|
25
|
+
successMessage?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Custom error message. If not provided, a default message will be used.
|
|
28
|
+
*/
|
|
29
|
+
errorMessage?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function RemoveFromChannelBulkAction({
|
|
33
|
+
selection,
|
|
34
|
+
table,
|
|
35
|
+
entityType,
|
|
36
|
+
mutationFn,
|
|
37
|
+
requiredPermissions,
|
|
38
|
+
buildInput,
|
|
39
|
+
onSuccess,
|
|
40
|
+
successMessage,
|
|
41
|
+
errorMessage,
|
|
42
|
+
}: Readonly<RemoveFromChannelBulkActionProps>) {
|
|
43
|
+
const { refetchPaginatedList } = usePaginatedList();
|
|
44
|
+
const { selectedChannel } = useChannel();
|
|
45
|
+
const { i18n } = useLingui();
|
|
46
|
+
const { mutate } = useMutation({
|
|
47
|
+
mutationFn,
|
|
48
|
+
onSuccess: result => {
|
|
49
|
+
const message =
|
|
50
|
+
successMessage ||
|
|
51
|
+
i18n.t(`Successfully removed ${selection.length} ${entityType} from channel`);
|
|
52
|
+
toast.success(message);
|
|
53
|
+
refetchPaginatedList();
|
|
54
|
+
table.resetRowSelection();
|
|
55
|
+
onSuccess?.(result);
|
|
56
|
+
},
|
|
57
|
+
onError: error => {
|
|
58
|
+
const message =
|
|
59
|
+
errorMessage ||
|
|
60
|
+
`Failed to remove ${selection.length} ${entityType} from channel: ${error.message}`;
|
|
61
|
+
toast.error(message);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!selectedChannel) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handleRemove = () => {
|
|
70
|
+
mutate({
|
|
71
|
+
input: buildInput(),
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<DataTableBulkActionItem
|
|
77
|
+
requiresPermission={requiredPermissions}
|
|
78
|
+
onClick={handleRemove}
|
|
79
|
+
label={<Trans>Remove from current channel</Trans>}
|
|
80
|
+
confirmationText={
|
|
81
|
+
<Trans>
|
|
82
|
+
Are you sure you want to remove {selection.length} {entityType} from the current channel?
|
|
83
|
+
</Trans>
|
|
84
|
+
}
|
|
85
|
+
icon={LayersIcon}
|
|
86
|
+
className="text-warning"
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|