@vendure/dashboard 3.3.6-master-202506280232 → 3.3.6-master-202507010243
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/routes/_authenticated/_collections/collections.graphql.ts +16 -0
- package/src/app/routes/_authenticated/_collections/collections.tsx +16 -2
- package/src/app/routes/_authenticated/_collections/components/assign-collections-to-channel-dialog.tsx +110 -0
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +99 -0
- package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +184 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +62 -1
- package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +33 -3
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +9 -2
- package/src/app/routes/_authenticated/_products/components/assign-facet-values-dialog.tsx +67 -36
- package/src/app/routes/_authenticated/_products/components/assign-to-channel-dialog.tsx +28 -17
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +12 -2
- package/src/app/routes/_authenticated/_products/components/product-variants-table.tsx +74 -55
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +1 -0
- package/src/lib/components/shared/detail-page-button.tsx +3 -1
- package/src/lib/components/shared/paginated-list-data-table.tsx +6 -4
- package/src/lib/framework/data-table/data-table-extensions.ts +14 -0
- package/src/lib/framework/document-extension/extend-document.spec.ts +549 -0
- package/src/lib/framework/document-extension/extend-document.ts +159 -0
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +14 -1
- package/src/lib/framework/extension-api/extension-api-types.ts +6 -0
- package/src/lib/framework/page/detail-page-route-loader.tsx +9 -3
- package/src/lib/framework/registry/registry-types.ts +2 -0
- package/src/lib/hooks/use-extended-list-query.ts +73 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.3.6-master-
|
|
4
|
+
"version": "3.3.6-master-202507010243",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"@types/react-dom": "^19.0.4",
|
|
87
87
|
"@types/react-grid-layout": "^1.3.5",
|
|
88
88
|
"@uidotdev/usehooks": "^2.4.1",
|
|
89
|
-
"@vendure/common": "^3.3.6-master-
|
|
90
|
-
"@vendure/core": "^3.3.6-master-
|
|
89
|
+
"@vendure/common": "^3.3.6-master-202507010243",
|
|
90
|
+
"@vendure/core": "^3.3.6-master-202507010243",
|
|
91
91
|
"@vitejs/plugin-react": "^4.3.4",
|
|
92
92
|
"awesome-graphql-client": "^2.1.0",
|
|
93
93
|
"class-variance-authority": "^0.7.1",
|
|
@@ -130,5 +130,5 @@
|
|
|
130
130
|
"lightningcss-linux-arm64-musl": "^1.29.3",
|
|
131
131
|
"lightningcss-linux-x64-musl": "^1.29.1"
|
|
132
132
|
},
|
|
133
|
-
"gitHead": "
|
|
133
|
+
"gitHead": "933c58e23163429de1540926c4f89ea201fbe31e"
|
|
134
134
|
}
|
|
@@ -131,3 +131,19 @@ export const getCollectionFiltersQueryOptions = queryOptions({
|
|
|
131
131
|
queryKey: ['getCollectionFilters'],
|
|
132
132
|
queryFn: () => api.query(getCollectionFiltersDocument),
|
|
133
133
|
}) as DefinedInitialDataOptions<ResultOf<typeof getCollectionFiltersDocument>>;
|
|
134
|
+
|
|
135
|
+
export const assignCollectionToChannelDocument = graphql(`
|
|
136
|
+
mutation AssignCollectionsToChannel($input: AssignCollectionsToChannelInput!) {
|
|
137
|
+
assignCollectionsToChannel(input: $input) {
|
|
138
|
+
id
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
`);
|
|
142
|
+
|
|
143
|
+
export const removeCollectionFromChannelDocument = graphql(`
|
|
144
|
+
mutation RemoveCollectionsFromChannel($input: RemoveCollectionsFromChannelInput!) {
|
|
145
|
+
removeCollectionsFromChannel(input: $input) {
|
|
146
|
+
id
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
`);
|
|
@@ -5,7 +5,7 @@ import { PageActionBarRight } from '@/framework/layout-engine/page-layout.js';
|
|
|
5
5
|
import { ListPage } from '@/framework/page/list-page.js';
|
|
6
6
|
import { api } from '@/graphql/api.js';
|
|
7
7
|
import { Trans } from '@/lib/trans.js';
|
|
8
|
-
import { useQueries } from '@tanstack/react-query';
|
|
8
|
+
import { FetchQueryOptions, useQueries } from '@tanstack/react-query';
|
|
9
9
|
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
10
10
|
import { ExpandedState, getExpandedRowModel } from '@tanstack/react-table';
|
|
11
11
|
import { TableOptions } from '@tanstack/table-core';
|
|
@@ -14,6 +14,10 @@ import { Folder, FolderOpen, PlusIcon } from 'lucide-react';
|
|
|
14
14
|
import { useState } from 'react';
|
|
15
15
|
|
|
16
16
|
import { collectionListDocument, deleteCollectionDocument } from './collections.graphql.js';
|
|
17
|
+
import {
|
|
18
|
+
AssignCollectionsToChannelBulkAction,
|
|
19
|
+
RemoveCollectionsFromChannelBulkAction,
|
|
20
|
+
} from './components/collection-bulk-actions.js';
|
|
17
21
|
import { CollectionContentsSheet } from './components/collection-contents-sheet.js';
|
|
18
22
|
|
|
19
23
|
export const Route = createFileRoute('/_authenticated/_collections/collections')({
|
|
@@ -38,7 +42,7 @@ function CollectionListPage() {
|
|
|
38
42
|
},
|
|
39
43
|
}),
|
|
40
44
|
staleTime: 1000 * 60 * 5,
|
|
41
|
-
};
|
|
45
|
+
} satisfies FetchQueryOptions;
|
|
42
46
|
}),
|
|
43
47
|
});
|
|
44
48
|
const childCollectionsByParentId = childrenQueries.reduce(
|
|
@@ -179,6 +183,16 @@ function CollectionListPage() {
|
|
|
179
183
|
};
|
|
180
184
|
}}
|
|
181
185
|
route={Route}
|
|
186
|
+
bulkActions={[
|
|
187
|
+
{
|
|
188
|
+
component: AssignCollectionsToChannelBulkAction,
|
|
189
|
+
order: 100,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
component: RemoveCollectionsFromChannelBulkAction,
|
|
193
|
+
order: 200,
|
|
194
|
+
},
|
|
195
|
+
]}
|
|
182
196
|
>
|
|
183
197
|
<PageActionBarRight>
|
|
184
198
|
<PermissionGuard requires={['CreateCollection', 'CreateCatalog']}>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useMutation } from '@tanstack/react-query';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { toast } from 'sonner';
|
|
4
|
+
|
|
5
|
+
import { ChannelCodeLabel } from '@/components/shared/channel-code-label.js';
|
|
6
|
+
import { Button } from '@/components/ui/button.js';
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
} from '@/components/ui/dialog.js';
|
|
15
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select.js';
|
|
16
|
+
import { ResultOf } from '@/graphql/graphql.js';
|
|
17
|
+
import { Trans, useLingui } from '@/lib/trans.js';
|
|
18
|
+
|
|
19
|
+
import { useChannel } from '@/hooks/use-channel.js';
|
|
20
|
+
|
|
21
|
+
interface AssignCollectionsToChannelDialogProps {
|
|
22
|
+
open: boolean;
|
|
23
|
+
onOpenChange: (open: boolean) => void;
|
|
24
|
+
entityIds: string[];
|
|
25
|
+
mutationFn: (variables: any) => Promise<ResultOf<any>>;
|
|
26
|
+
onSuccess?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function AssignCollectionsToChannelDialog({
|
|
30
|
+
open,
|
|
31
|
+
onOpenChange,
|
|
32
|
+
entityIds,
|
|
33
|
+
mutationFn,
|
|
34
|
+
onSuccess,
|
|
35
|
+
}: AssignCollectionsToChannelDialogProps) {
|
|
36
|
+
const { i18n } = useLingui();
|
|
37
|
+
const [selectedChannelId, setSelectedChannelId] = useState<string>('');
|
|
38
|
+
const { channels, selectedChannel } = useChannel();
|
|
39
|
+
|
|
40
|
+
// Filter out the currently selected channel from available options
|
|
41
|
+
const availableChannels = channels.filter(channel => channel.id !== selectedChannel?.id);
|
|
42
|
+
|
|
43
|
+
const { mutate, isPending } = useMutation({
|
|
44
|
+
mutationFn,
|
|
45
|
+
onSuccess: () => {
|
|
46
|
+
toast.success(i18n.t(`Successfully assigned ${entityIds.length} collections to channel`));
|
|
47
|
+
onSuccess?.();
|
|
48
|
+
onOpenChange(false);
|
|
49
|
+
},
|
|
50
|
+
onError: () => {
|
|
51
|
+
toast.error(`Failed to assign ${entityIds.length} collections to channel`);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const handleAssign = () => {
|
|
56
|
+
if (!selectedChannelId) {
|
|
57
|
+
toast.error('Please select a channel');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const input = {
|
|
62
|
+
collectionIds: entityIds,
|
|
63
|
+
channelId: selectedChannelId,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
mutate({ input });
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
71
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
72
|
+
<DialogHeader>
|
|
73
|
+
<DialogTitle>
|
|
74
|
+
<Trans>Assign collections to channel</Trans>
|
|
75
|
+
</DialogTitle>
|
|
76
|
+
<DialogDescription>
|
|
77
|
+
<Trans>Select a channel to assign {entityIds.length} collections to</Trans>
|
|
78
|
+
</DialogDescription>
|
|
79
|
+
</DialogHeader>
|
|
80
|
+
<div className="grid gap-4 py-4">
|
|
81
|
+
<div className="grid gap-2">
|
|
82
|
+
<label className="text-sm font-medium">
|
|
83
|
+
<Trans>Channel</Trans>
|
|
84
|
+
</label>
|
|
85
|
+
<Select value={selectedChannelId} onValueChange={setSelectedChannelId}>
|
|
86
|
+
<SelectTrigger>
|
|
87
|
+
<SelectValue placeholder={i18n.t('Select a channel')} />
|
|
88
|
+
</SelectTrigger>
|
|
89
|
+
<SelectContent>
|
|
90
|
+
{availableChannels.map(channel => (
|
|
91
|
+
<SelectItem key={channel.id} value={channel.id}>
|
|
92
|
+
<ChannelCodeLabel code={channel.code} />
|
|
93
|
+
</SelectItem>
|
|
94
|
+
))}
|
|
95
|
+
</SelectContent>
|
|
96
|
+
</Select>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<DialogFooter>
|
|
100
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
101
|
+
<Trans>Cancel</Trans>
|
|
102
|
+
</Button>
|
|
103
|
+
<Button onClick={handleAssign} disabled={!selectedChannelId || isPending}>
|
|
104
|
+
<Trans>Assign</Trans>
|
|
105
|
+
</Button>
|
|
106
|
+
</DialogFooter>
|
|
107
|
+
</DialogContent>
|
|
108
|
+
</Dialog>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { LayersIcon } from 'lucide-react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
|
|
6
|
+
import { DataTableBulkActionItem } from '@/components/data-table/data-table-bulk-action-item.js';
|
|
7
|
+
import { BulkActionComponent } from '@/framework/data-table/data-table-types.js';
|
|
8
|
+
import { api } from '@/graphql/api.js';
|
|
9
|
+
import { useChannel, usePaginatedList } from '@/index.js';
|
|
10
|
+
import { Trans, useLingui } from '@/lib/trans.js';
|
|
11
|
+
|
|
12
|
+
import { Permission } from '@vendure/common/lib/generated-types';
|
|
13
|
+
import {
|
|
14
|
+
assignCollectionToChannelDocument,
|
|
15
|
+
removeCollectionFromChannelDocument,
|
|
16
|
+
} from '../collections.graphql.js';
|
|
17
|
+
import { AssignCollectionsToChannelDialog } from './assign-collections-to-channel-dialog.js';
|
|
18
|
+
|
|
19
|
+
export const AssignCollectionsToChannelBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
|
|
20
|
+
const { refetchPaginatedList } = usePaginatedList();
|
|
21
|
+
const { channels } = useChannel();
|
|
22
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
23
|
+
const queryClient = useQueryClient();
|
|
24
|
+
|
|
25
|
+
if (channels.length < 2) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const handleSuccess = () => {
|
|
30
|
+
refetchPaginatedList();
|
|
31
|
+
table.resetRowSelection();
|
|
32
|
+
queryClient.invalidateQueries({ queryKey: ['childCollections'] });
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<DataTableBulkActionItem
|
|
38
|
+
requiresPermission={[Permission.UpdateCatalog, Permission.UpdateCollection]}
|
|
39
|
+
onClick={() => setDialogOpen(true)}
|
|
40
|
+
label={<Trans>Assign to channel</Trans>}
|
|
41
|
+
icon={LayersIcon}
|
|
42
|
+
/>
|
|
43
|
+
<AssignCollectionsToChannelDialog
|
|
44
|
+
open={dialogOpen}
|
|
45
|
+
onOpenChange={setDialogOpen}
|
|
46
|
+
entityIds={selection.map(s => s.id)}
|
|
47
|
+
mutationFn={api.mutate(assignCollectionToChannelDocument)}
|
|
48
|
+
onSuccess={handleSuccess}
|
|
49
|
+
/>
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const RemoveCollectionsFromChannelBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
|
|
55
|
+
const { refetchPaginatedList } = usePaginatedList();
|
|
56
|
+
const { selectedChannel } = useChannel();
|
|
57
|
+
const { i18n } = useLingui();
|
|
58
|
+
const queryClient = useQueryClient();
|
|
59
|
+
const { mutate } = useMutation({
|
|
60
|
+
mutationFn: api.mutate(removeCollectionFromChannelDocument),
|
|
61
|
+
onSuccess: () => {
|
|
62
|
+
toast.success(i18n.t(`Successfully removed ${selection.length} collections from channel`));
|
|
63
|
+
refetchPaginatedList();
|
|
64
|
+
table.resetRowSelection();
|
|
65
|
+
queryClient.invalidateQueries({ queryKey: ['childCollections'] });
|
|
66
|
+
},
|
|
67
|
+
onError: error => {
|
|
68
|
+
toast.error(`Failed to remove ${selection.length} collections from channel: ${error.message}`);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!selectedChannel) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const handleRemove = () => {
|
|
77
|
+
mutate({
|
|
78
|
+
input: {
|
|
79
|
+
collectionIds: selection.map(s => s.id),
|
|
80
|
+
channelId: selectedChannel.id,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<DataTableBulkActionItem
|
|
87
|
+
requiresPermission={[Permission.UpdateCatalog, Permission.UpdateCollection]}
|
|
88
|
+
onClick={handleRemove}
|
|
89
|
+
label={<Trans>Remove from current channel</Trans>}
|
|
90
|
+
confirmationText={
|
|
91
|
+
<Trans>
|
|
92
|
+
Are you sure you want to remove {selection.length} collections from the current channel?
|
|
93
|
+
</Trans>
|
|
94
|
+
}
|
|
95
|
+
icon={LayersIcon}
|
|
96
|
+
className="text-warning"
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
};
|
package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { useMutation } from '@tanstack/react-query';
|
|
2
|
+
import { LayersIcon, TagIcon, TrashIcon } from 'lucide-react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
|
|
6
|
+
import { DataTableBulkActionItem } from '@/components/data-table/data-table-bulk-action-item.js';
|
|
7
|
+
import { BulkActionComponent } from '@/framework/data-table/data-table-types.js';
|
|
8
|
+
import { api } from '@/graphql/api.js';
|
|
9
|
+
import { ResultOf } from '@/graphql/graphql.js';
|
|
10
|
+
import { useChannel, usePaginatedList } from '@/index.js';
|
|
11
|
+
import { Trans, useLingui } from '@/lib/trans.js';
|
|
12
|
+
|
|
13
|
+
import { Permission } from '@vendure/common/lib/generated-types';
|
|
14
|
+
import { AssignFacetValuesDialog } from '../../_products/components/assign-facet-values-dialog.js';
|
|
15
|
+
import { AssignToChannelDialog } from '../../_products/components/assign-to-channel-dialog.js';
|
|
16
|
+
import {
|
|
17
|
+
assignProductVariantsToChannelDocument,
|
|
18
|
+
deleteProductVariantsDocument,
|
|
19
|
+
getProductVariantsWithFacetValuesByIdsDocument,
|
|
20
|
+
productVariantDetailDocument,
|
|
21
|
+
removeProductVariantsFromChannelDocument,
|
|
22
|
+
updateProductVariantsDocument,
|
|
23
|
+
} from '../product-variants.graphql.js';
|
|
24
|
+
|
|
25
|
+
export const DeleteProductVariantsBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
|
|
26
|
+
const { refetchPaginatedList } = usePaginatedList();
|
|
27
|
+
const { i18n } = useLingui();
|
|
28
|
+
const { mutate } = useMutation({
|
|
29
|
+
mutationFn: api.mutate(deleteProductVariantsDocument),
|
|
30
|
+
onSuccess: (result: ResultOf<typeof deleteProductVariantsDocument>) => {
|
|
31
|
+
let deleted = 0;
|
|
32
|
+
const errors: string[] = [];
|
|
33
|
+
for (const item of result.deleteProductVariants) {
|
|
34
|
+
if (item.result === 'DELETED') {
|
|
35
|
+
deleted++;
|
|
36
|
+
} else if (item.message) {
|
|
37
|
+
errors.push(item.message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (0 < deleted) {
|
|
41
|
+
toast.success(i18n.t(`Deleted ${deleted} product variants`));
|
|
42
|
+
}
|
|
43
|
+
if (0 < errors.length) {
|
|
44
|
+
toast.error(i18n.t(`Failed to delete ${errors.length} product variants`));
|
|
45
|
+
}
|
|
46
|
+
refetchPaginatedList();
|
|
47
|
+
table.resetRowSelection();
|
|
48
|
+
},
|
|
49
|
+
onError: () => {
|
|
50
|
+
toast.error(`Failed to delete ${selection.length} product variants`);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
return (
|
|
54
|
+
<DataTableBulkActionItem
|
|
55
|
+
requiresPermission={[Permission.DeleteCatalog, Permission.DeleteProduct]}
|
|
56
|
+
onClick={() => mutate({ ids: selection.map(s => s.id) })}
|
|
57
|
+
label={<Trans>Delete</Trans>}
|
|
58
|
+
confirmationText={
|
|
59
|
+
<Trans>Are you sure you want to delete {selection.length} product variants?</Trans>
|
|
60
|
+
}
|
|
61
|
+
icon={TrashIcon}
|
|
62
|
+
className="text-destructive"
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const AssignProductVariantsToChannelBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
|
|
68
|
+
const { refetchPaginatedList } = usePaginatedList();
|
|
69
|
+
const { channels } = useChannel();
|
|
70
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
71
|
+
|
|
72
|
+
if (channels.length < 2) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const handleSuccess = () => {
|
|
77
|
+
refetchPaginatedList();
|
|
78
|
+
table.resetRowSelection();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<>
|
|
83
|
+
<DataTableBulkActionItem
|
|
84
|
+
requiresPermission={[Permission.UpdateCatalog, Permission.UpdateProduct]}
|
|
85
|
+
onClick={() => setDialogOpen(true)}
|
|
86
|
+
label={<Trans>Assign to channel</Trans>}
|
|
87
|
+
icon={LayersIcon}
|
|
88
|
+
/>
|
|
89
|
+
<AssignToChannelDialog
|
|
90
|
+
open={dialogOpen}
|
|
91
|
+
onOpenChange={setDialogOpen}
|
|
92
|
+
entityIds={selection.map(s => s.id)}
|
|
93
|
+
entityType="variants"
|
|
94
|
+
mutationFn={api.mutate(assignProductVariantsToChannelDocument)}
|
|
95
|
+
onSuccess={handleSuccess}
|
|
96
|
+
/>
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const RemoveProductVariantsFromChannelBulkAction: BulkActionComponent<any> = ({
|
|
102
|
+
selection,
|
|
103
|
+
table,
|
|
104
|
+
}) => {
|
|
105
|
+
const { refetchPaginatedList } = usePaginatedList();
|
|
106
|
+
const { selectedChannel } = useChannel();
|
|
107
|
+
const { i18n } = useLingui();
|
|
108
|
+
const { mutate } = useMutation({
|
|
109
|
+
mutationFn: api.mutate(removeProductVariantsFromChannelDocument),
|
|
110
|
+
onSuccess: () => {
|
|
111
|
+
toast.success(i18n.t(`Successfully removed ${selection.length} product variants from channel`));
|
|
112
|
+
refetchPaginatedList();
|
|
113
|
+
table.resetRowSelection();
|
|
114
|
+
},
|
|
115
|
+
onError: error => {
|
|
116
|
+
toast.error(
|
|
117
|
+
`Failed to remove ${selection.length} product variants from channel: ${error.message}`,
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!selectedChannel) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const handleRemove = () => {
|
|
127
|
+
mutate({
|
|
128
|
+
input: {
|
|
129
|
+
productVariantIds: selection.map(s => s.id),
|
|
130
|
+
channelId: selectedChannel.id,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<DataTableBulkActionItem
|
|
137
|
+
requiresPermission={[Permission.UpdateCatalog, Permission.UpdateProduct]}
|
|
138
|
+
onClick={handleRemove}
|
|
139
|
+
label={<Trans>Remove from current channel</Trans>}
|
|
140
|
+
confirmationText={
|
|
141
|
+
<Trans>
|
|
142
|
+
Are you sure you want to remove {selection.length} product variants from the current
|
|
143
|
+
channel?
|
|
144
|
+
</Trans>
|
|
145
|
+
}
|
|
146
|
+
icon={LayersIcon}
|
|
147
|
+
className="text-warning"
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const AssignFacetValuesToProductVariantsBulkAction: BulkActionComponent<any> = ({
|
|
153
|
+
selection,
|
|
154
|
+
table,
|
|
155
|
+
}) => {
|
|
156
|
+
const { refetchPaginatedList } = usePaginatedList();
|
|
157
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
158
|
+
|
|
159
|
+
const handleSuccess = () => {
|
|
160
|
+
refetchPaginatedList();
|
|
161
|
+
table.resetRowSelection();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<>
|
|
166
|
+
<DataTableBulkActionItem
|
|
167
|
+
requiresPermission={[Permission.UpdateCatalog, Permission.UpdateProduct]}
|
|
168
|
+
onClick={() => setDialogOpen(true)}
|
|
169
|
+
label={<Trans>Edit facet values</Trans>}
|
|
170
|
+
icon={TagIcon}
|
|
171
|
+
/>
|
|
172
|
+
<AssignFacetValuesDialog
|
|
173
|
+
open={dialogOpen}
|
|
174
|
+
onOpenChange={setDialogOpen}
|
|
175
|
+
entityIds={selection.map(s => s.id)}
|
|
176
|
+
entityType="variants"
|
|
177
|
+
queryFn={variables => api.query(getProductVariantsWithFacetValuesByIdsDocument, variables)}
|
|
178
|
+
mutationFn={api.mutate(updateProductVariantsDocument)}
|
|
179
|
+
detailDocument={productVariantDetailDocument}
|
|
180
|
+
onSuccess={handleSuccess}
|
|
181
|
+
/>
|
|
182
|
+
</>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
@@ -3,7 +3,7 @@ import { graphql } from '@/graphql/graphql.js';
|
|
|
3
3
|
|
|
4
4
|
export const productVariantListDocument = graphql(
|
|
5
5
|
`
|
|
6
|
-
query
|
|
6
|
+
query ProductVariantList($options: ProductVariantListOptions) {
|
|
7
7
|
productVariants(options: $options) {
|
|
8
8
|
items {
|
|
9
9
|
id
|
|
@@ -121,3 +121,64 @@ export const deleteProductVariantDocument = graphql(`
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
`);
|
|
124
|
+
|
|
125
|
+
export const deleteProductVariantsDocument = graphql(`
|
|
126
|
+
mutation DeleteProductVariants($ids: [ID!]!) {
|
|
127
|
+
deleteProductVariants(ids: $ids) {
|
|
128
|
+
result
|
|
129
|
+
message
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
`);
|
|
133
|
+
|
|
134
|
+
export const assignProductVariantsToChannelDocument = graphql(`
|
|
135
|
+
mutation AssignProductVariantsToChannel($input: AssignProductVariantsToChannelInput!) {
|
|
136
|
+
assignProductVariantsToChannel(input: $input) {
|
|
137
|
+
id
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
`);
|
|
141
|
+
|
|
142
|
+
export const removeProductVariantsFromChannelDocument = graphql(`
|
|
143
|
+
mutation RemoveProductVariantsFromChannel($input: RemoveProductVariantsFromChannelInput!) {
|
|
144
|
+
removeProductVariantsFromChannel(input: $input) {
|
|
145
|
+
id
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
`);
|
|
149
|
+
|
|
150
|
+
export const getProductVariantsWithFacetValuesByIdsDocument = graphql(`
|
|
151
|
+
query GetProductVariantsWithFacetValuesByIds($ids: [String!]!) {
|
|
152
|
+
productVariants(options: { filter: { id: { in: $ids } } }) {
|
|
153
|
+
items {
|
|
154
|
+
id
|
|
155
|
+
name
|
|
156
|
+
sku
|
|
157
|
+
facetValues {
|
|
158
|
+
id
|
|
159
|
+
name
|
|
160
|
+
code
|
|
161
|
+
facet {
|
|
162
|
+
id
|
|
163
|
+
name
|
|
164
|
+
code
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
`);
|
|
171
|
+
|
|
172
|
+
export const updateProductVariantsDocument = graphql(`
|
|
173
|
+
mutation UpdateProductVariants($input: [UpdateProductVariantInput!]!) {
|
|
174
|
+
updateProductVariants(input: $input) {
|
|
175
|
+
id
|
|
176
|
+
name
|
|
177
|
+
facetValues {
|
|
178
|
+
id
|
|
179
|
+
name
|
|
180
|
+
code
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
`);
|
|
@@ -5,6 +5,12 @@ import { ListPage } from '@/framework/page/list-page.js';
|
|
|
5
5
|
import { useLocalFormat } from '@/hooks/use-local-format.js';
|
|
6
6
|
import { Trans } from '@/lib/trans.js';
|
|
7
7
|
import { createFileRoute } from '@tanstack/react-router';
|
|
8
|
+
import {
|
|
9
|
+
AssignFacetValuesToProductVariantsBulkAction,
|
|
10
|
+
AssignProductVariantsToChannelBulkAction,
|
|
11
|
+
DeleteProductVariantsBulkAction,
|
|
12
|
+
RemoveProductVariantsFromChannelBulkAction,
|
|
13
|
+
} from './components/product-variant-bulk-actions.js';
|
|
8
14
|
import { deleteProductVariantDocument, productVariantListDocument } from './product-variants.graphql.js';
|
|
9
15
|
|
|
10
16
|
export const Route = createFileRoute('/_authenticated/_product-variants/product-variants')({
|
|
@@ -20,19 +26,43 @@ function ProductListPage() {
|
|
|
20
26
|
title={<Trans>Product Variants</Trans>}
|
|
21
27
|
listQuery={productVariantListDocument}
|
|
22
28
|
deleteMutation={deleteProductVariantDocument}
|
|
29
|
+
bulkActions={[
|
|
30
|
+
{
|
|
31
|
+
component: AssignProductVariantsToChannelBulkAction,
|
|
32
|
+
order: 100,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
component: RemoveProductVariantsFromChannelBulkAction,
|
|
36
|
+
order: 200,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
component: AssignFacetValuesToProductVariantsBulkAction,
|
|
40
|
+
order: 300,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
component: DeleteProductVariantsBulkAction,
|
|
44
|
+
order: 400,
|
|
45
|
+
},
|
|
46
|
+
]}
|
|
23
47
|
customizeColumns={{
|
|
24
48
|
name: {
|
|
25
49
|
header: 'Product Name',
|
|
26
|
-
cell: ({ row: { original } }) =>
|
|
50
|
+
cell: ({ row: { original } }) => (
|
|
51
|
+
<DetailPageButton id={original.id} label={original.name} />
|
|
52
|
+
),
|
|
27
53
|
},
|
|
28
54
|
currencyCode: {
|
|
29
55
|
cell: ({ row: { original } }) => formatCurrencyName(original.currencyCode, 'full'),
|
|
30
56
|
},
|
|
31
57
|
price: {
|
|
32
|
-
cell: ({ row: { original } }) =>
|
|
58
|
+
cell: ({ row: { original } }) => (
|
|
59
|
+
<Money value={original.price} currency={original.currencyCode} />
|
|
60
|
+
),
|
|
33
61
|
},
|
|
34
62
|
priceWithTax: {
|
|
35
|
-
cell: ({ row: { original } }) =>
|
|
63
|
+
cell: ({ row: { original } }) => (
|
|
64
|
+
<Money value={original.priceWithTax} currency={original.currencyCode} />
|
|
65
|
+
),
|
|
36
66
|
},
|
|
37
67
|
stockLevels: {
|
|
38
68
|
cell: ({ row: { original } }) => <StockLevelLabel stockLevels={original.stockLevels} />,
|
|
@@ -40,8 +40,15 @@ export const Route = createFileRoute('/_authenticated/_product-variants/product-
|
|
|
40
40
|
component: ProductVariantDetailPage,
|
|
41
41
|
loader: detailPageRouteLoader({
|
|
42
42
|
queryDocument: productVariantDetailDocument,
|
|
43
|
-
breadcrumb(_isNew, entity) {
|
|
44
|
-
|
|
43
|
+
breadcrumb(_isNew, entity, location) {
|
|
44
|
+
if ((location.search as any).from === 'product') {
|
|
45
|
+
return [
|
|
46
|
+
{ path: '/product', label: 'Products' },
|
|
47
|
+
{ path: `/products/${entity?.product.id}`, label: entity?.product.name ?? '' },
|
|
48
|
+
entity?.name,
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
return [{ path: '/product-variants', label: 'Product Variants' }, entity?.name];
|
|
45
52
|
},
|
|
46
53
|
}),
|
|
47
54
|
errorComponent: ({ error }) => <ErrorPage message={error.message} />,
|