@vendure/dashboard 3.3.6-master-202506290242 → 3.3.6-master-202507010731
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/_administrators/administrators_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +7 -2
- package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +5 -1
- 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/collections_.$id.tsx +5 -1
- 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/_countries/countries.graphql.ts +1 -1
- package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +9 -5
- package/src/app/routes/_authenticated/_customer-groups/customer-groups.graphql.ts +1 -1
- package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +8 -5
- package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +5 -2
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +5 -1
- 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 +14 -3
- 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 +6 -1
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +6 -2
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_tax-categories/tax-categories.graphql.ts +1 -1
- package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +9 -5
- package/src/app/routes/_authenticated/_tax-rates/tax-rates.graphql.ts +1 -1
- package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +8 -4
- package/src/app/routes/_authenticated/_zones/zones.graphql.ts +1 -1
- package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +8 -4
- 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-detail-form-query.ts +50 -0
- package/src/lib/framework/document-extension/extend-document.spec.ts +884 -0
- package/src/lib/framework/document-extension/extend-document.ts +159 -0
- package/src/lib/framework/document-introspection/add-custom-fields.ts +48 -0
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +33 -2
- package/src/lib/framework/extension-api/extension-api-types.ts +21 -2
- package/src/lib/framework/form-engine/custom-form-component-extensions.ts +13 -3
- package/src/lib/framework/layout-engine/page-layout.tsx +1 -0
- package/src/lib/framework/page/detail-page-route-loader.tsx +22 -4
- package/src/lib/framework/page/use-detail-page.ts +11 -2
- package/src/lib/framework/registry/registry-types.ts +3 -0
- package/src/lib/graphql/graphql-env.d.ts +8 -6
- package/src/lib/hooks/use-extended-detail-query.ts +37 -0
- package/src/lib/hooks/use-extended-list-query.ts +73 -0
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} />,
|
|
@@ -36,12 +36,22 @@ import {
|
|
|
36
36
|
updateProductVariantDocument,
|
|
37
37
|
} from './product-variants.graphql.js';
|
|
38
38
|
|
|
39
|
+
const pageId = 'product-variant-detail';
|
|
40
|
+
|
|
39
41
|
export const Route = createFileRoute('/_authenticated/_product-variants/product-variants_/$id')({
|
|
40
42
|
component: ProductVariantDetailPage,
|
|
41
43
|
loader: detailPageRouteLoader({
|
|
44
|
+
pageId,
|
|
42
45
|
queryDocument: productVariantDetailDocument,
|
|
43
|
-
breadcrumb(_isNew, entity) {
|
|
44
|
-
|
|
46
|
+
breadcrumb(_isNew, entity, location) {
|
|
47
|
+
if ((location.search as any).from === 'product') {
|
|
48
|
+
return [
|
|
49
|
+
{ path: '/product', label: 'Products' },
|
|
50
|
+
{ path: `/products/${entity?.product.id}`, label: entity?.product.name ?? '' },
|
|
51
|
+
entity?.name,
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
return [{ path: '/product-variants', label: 'Product Variants' }, entity?.name];
|
|
45
55
|
},
|
|
46
56
|
}),
|
|
47
57
|
errorComponent: ({ error }) => <ErrorPage message={error.message} />,
|
|
@@ -55,6 +65,7 @@ function ProductVariantDetailPage() {
|
|
|
55
65
|
const { activeChannel } = useChannel();
|
|
56
66
|
|
|
57
67
|
const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
|
|
68
|
+
pageId,
|
|
58
69
|
queryDocument: productVariantDetailDocument,
|
|
59
70
|
createDocument: createProductVariantDocument,
|
|
60
71
|
updateDocument: updateProductVariantDocument,
|
|
@@ -102,7 +113,7 @@ function ProductVariantDetailPage() {
|
|
|
102
113
|
const [price, taxCategoryId] = form.watch(['price', 'taxCategoryId']);
|
|
103
114
|
|
|
104
115
|
return (
|
|
105
|
-
<Page pageId=
|
|
116
|
+
<Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
|
|
106
117
|
<PageTitle>
|
|
107
118
|
{creatingNewEntity ? <Trans>New product variant</Trans> : (entity?.name ?? '')}
|
|
108
119
|
</PageTitle>
|
|
@@ -13,20 +13,15 @@ import {
|
|
|
13
13
|
DialogHeader,
|
|
14
14
|
DialogTitle,
|
|
15
15
|
} from '@/components/ui/dialog.js';
|
|
16
|
-
import { api } from '@/graphql/api.js';
|
|
17
16
|
import { ResultOf } from '@/graphql/graphql.js';
|
|
18
17
|
import { Trans, useLingui } from '@/lib/trans.js';
|
|
19
18
|
|
|
20
19
|
import { getDetailQueryOptions } from '@/framework/page/use-detail-page.js';
|
|
21
|
-
import {
|
|
22
|
-
getProductsWithFacetValuesByIdsDocument,
|
|
23
|
-
productDetailDocument,
|
|
24
|
-
updateProductsDocument,
|
|
25
|
-
} from '../products.graphql.js';
|
|
26
20
|
|
|
27
|
-
interface
|
|
21
|
+
interface EntityWithFacetValues {
|
|
28
22
|
id: string;
|
|
29
23
|
name: string;
|
|
24
|
+
sku?: string;
|
|
30
25
|
facetValues: Array<{
|
|
31
26
|
id: string;
|
|
32
27
|
name: string;
|
|
@@ -42,14 +37,22 @@ interface ProductWithFacetValues {
|
|
|
42
37
|
interface AssignFacetValuesDialogProps {
|
|
43
38
|
open: boolean;
|
|
44
39
|
onOpenChange: (open: boolean) => void;
|
|
45
|
-
|
|
40
|
+
entityIds: string[];
|
|
41
|
+
entityType: 'products' | 'variants';
|
|
42
|
+
queryFn: (variables: any) => Promise<ResultOf<any>>;
|
|
43
|
+
mutationFn: (variables: any) => Promise<ResultOf<any>>;
|
|
44
|
+
detailDocument: any;
|
|
46
45
|
onSuccess?: () => void;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
export function AssignFacetValuesDialog({
|
|
50
49
|
open,
|
|
51
50
|
onOpenChange,
|
|
52
|
-
|
|
51
|
+
entityIds,
|
|
52
|
+
entityType,
|
|
53
|
+
queryFn,
|
|
54
|
+
mutationFn,
|
|
55
|
+
detailDocument,
|
|
53
56
|
onSuccess,
|
|
54
57
|
}: AssignFacetValuesDialogProps) {
|
|
55
58
|
const { i18n } = useLingui();
|
|
@@ -58,30 +61,30 @@ export function AssignFacetValuesDialog({
|
|
|
58
61
|
const [removedFacetValues, setRemovedFacetValues] = useState<Set<string>>(new Set());
|
|
59
62
|
const queryClient = useQueryClient();
|
|
60
63
|
|
|
61
|
-
// Fetch existing facet values for the
|
|
62
|
-
const { data:
|
|
63
|
-
queryKey: [
|
|
64
|
-
queryFn: () =>
|
|
65
|
-
enabled: open &&
|
|
64
|
+
// Fetch existing facet values for the entities
|
|
65
|
+
const { data: entitiesData, isLoading } = useQuery({
|
|
66
|
+
queryKey: [`${entityType}WithFacetValues`, entityIds],
|
|
67
|
+
queryFn: () => queryFn({ ids: entityIds }),
|
|
68
|
+
enabled: open && entityIds.length > 0,
|
|
66
69
|
});
|
|
67
70
|
|
|
68
71
|
const { mutate, isPending } = useMutation({
|
|
69
|
-
mutationFn
|
|
70
|
-
onSuccess: (
|
|
71
|
-
toast.success(i18n.t(`Successfully updated facet values for ${
|
|
72
|
+
mutationFn,
|
|
73
|
+
onSuccess: () => {
|
|
74
|
+
toast.success(i18n.t(`Successfully updated facet values for ${entityIds.length} ${entityType}`));
|
|
72
75
|
onSuccess?.();
|
|
73
76
|
onOpenChange(false);
|
|
74
77
|
// Reset state
|
|
75
78
|
setSelectedValues([]);
|
|
76
79
|
setFacetValuesRemoved(false);
|
|
77
80
|
setRemovedFacetValues(new Set());
|
|
78
|
-
|
|
79
|
-
const { queryKey } = getDetailQueryOptions(
|
|
81
|
+
entityIds.forEach(id => {
|
|
82
|
+
const { queryKey } = getDetailQueryOptions(detailDocument, { id });
|
|
80
83
|
queryClient.removeQueries({ queryKey });
|
|
81
84
|
});
|
|
82
85
|
},
|
|
83
86
|
onError: () => {
|
|
84
|
-
toast.error(`Failed to update facet values for ${
|
|
87
|
+
toast.error(`Failed to update facet values for ${entityIds.length} ${entityType}`);
|
|
85
88
|
},
|
|
86
89
|
});
|
|
87
90
|
|
|
@@ -91,18 +94,25 @@ export function AssignFacetValuesDialog({
|
|
|
91
94
|
return;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
const items =
|
|
98
|
+
entityType === 'products'
|
|
99
|
+
? (entitiesData as any)?.products?.items
|
|
100
|
+
: (entitiesData as any)?.productVariants?.items;
|
|
101
|
+
|
|
102
|
+
if (!items) {
|
|
95
103
|
return;
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
const selectedFacetValueIds = selectedValues.map(sv => sv.id);
|
|
99
107
|
|
|
100
108
|
mutate({
|
|
101
|
-
input:
|
|
102
|
-
id:
|
|
109
|
+
input: items.map((entity: EntityWithFacetValues) => ({
|
|
110
|
+
id: entity.id,
|
|
103
111
|
facetValueIds: [
|
|
104
112
|
...new Set([
|
|
105
|
-
...
|
|
113
|
+
...entity.facetValues
|
|
114
|
+
.filter((fv: any) => !removedFacetValues.has(fv.id))
|
|
115
|
+
.map((fv: any) => fv.id),
|
|
106
116
|
...selectedFacetValueIds,
|
|
107
117
|
]),
|
|
108
118
|
],
|
|
@@ -114,7 +124,7 @@ export function AssignFacetValuesDialog({
|
|
|
114
124
|
setSelectedValues(prev => [...prev, facetValue]);
|
|
115
125
|
};
|
|
116
126
|
|
|
117
|
-
const removeFacetValue = (
|
|
127
|
+
const removeFacetValue = (entityId: string, facetValueId: string) => {
|
|
118
128
|
setRemovedFacetValues(prev => new Set([...prev, facetValueId]));
|
|
119
129
|
setFacetValuesRemoved(true);
|
|
120
130
|
};
|
|
@@ -128,10 +138,15 @@ export function AssignFacetValuesDialog({
|
|
|
128
138
|
};
|
|
129
139
|
|
|
130
140
|
// Filter out removed facet values for display
|
|
131
|
-
const getDisplayFacetValues = (
|
|
132
|
-
return
|
|
141
|
+
const getDisplayFacetValues = (entity: EntityWithFacetValues) => {
|
|
142
|
+
return entity.facetValues.filter(fv => !removedFacetValues.has(fv.id));
|
|
133
143
|
};
|
|
134
144
|
|
|
145
|
+
const items =
|
|
146
|
+
entityType === 'products'
|
|
147
|
+
? (entitiesData as any)?.products?.items
|
|
148
|
+
: (entitiesData as any)?.productVariants?.items;
|
|
149
|
+
|
|
135
150
|
return (
|
|
136
151
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
137
152
|
<DialogContent className="sm:max-w-[800px] max-h-[80vh] overflow-hidden flex flex-col">
|
|
@@ -140,7 +155,9 @@ export function AssignFacetValuesDialog({
|
|
|
140
155
|
<Trans>Edit facet values</Trans>
|
|
141
156
|
</DialogTitle>
|
|
142
157
|
<DialogDescription>
|
|
143
|
-
<Trans>
|
|
158
|
+
<Trans>
|
|
159
|
+
Add or remove facet values for {entityIds.length} {entityType}
|
|
160
|
+
</Trans>
|
|
144
161
|
</DialogDescription>
|
|
145
162
|
</DialogHeader>
|
|
146
163
|
|
|
@@ -156,33 +173,47 @@ export function AssignFacetValuesDialog({
|
|
|
156
173
|
/>
|
|
157
174
|
</div>
|
|
158
175
|
|
|
159
|
-
{/*
|
|
176
|
+
{/* Entities table */}
|
|
160
177
|
<div className="flex-1 overflow-auto">
|
|
161
178
|
{isLoading ? (
|
|
162
179
|
<div className="flex items-center justify-center py-8">
|
|
163
180
|
<div className="text-sm text-muted-foreground">Loading...</div>
|
|
164
181
|
</div>
|
|
165
|
-
) :
|
|
182
|
+
) : items ? (
|
|
166
183
|
<div className="border rounded-md">
|
|
167
184
|
<table className="w-full">
|
|
168
185
|
<thead className="bg-muted/50">
|
|
169
186
|
<tr>
|
|
170
187
|
<th className="text-left p-3 text-sm font-medium">
|
|
171
|
-
<Trans>
|
|
188
|
+
<Trans>
|
|
189
|
+
{entityType === 'products' ? 'Product' : 'Variant'}
|
|
190
|
+
</Trans>
|
|
172
191
|
</th>
|
|
192
|
+
{entityType === 'variants' && (
|
|
193
|
+
<th className="text-left p-3 text-sm font-medium">
|
|
194
|
+
<Trans>SKU</Trans>
|
|
195
|
+
</th>
|
|
196
|
+
)}
|
|
173
197
|
<th className="text-left p-3 text-sm font-medium">
|
|
174
198
|
<Trans>Current facet values</Trans>
|
|
175
199
|
</th>
|
|
176
200
|
</tr>
|
|
177
201
|
</thead>
|
|
178
202
|
<tbody>
|
|
179
|
-
{
|
|
180
|
-
const displayFacetValues = getDisplayFacetValues(
|
|
203
|
+
{items.map((entity: EntityWithFacetValues) => {
|
|
204
|
+
const displayFacetValues = getDisplayFacetValues(entity);
|
|
181
205
|
return (
|
|
182
|
-
<tr key={
|
|
206
|
+
<tr key={entity.id} className="border-t">
|
|
183
207
|
<td className="p-3 align-top">
|
|
184
|
-
<div className="font-medium">{
|
|
208
|
+
<div className="font-medium">{entity.name}</div>
|
|
185
209
|
</td>
|
|
210
|
+
{entityType === 'variants' && (
|
|
211
|
+
<td className="p-3 align-top">
|
|
212
|
+
<div className="text-sm text-muted-foreground">
|
|
213
|
+
{entity.sku}
|
|
214
|
+
</div>
|
|
215
|
+
</td>
|
|
216
|
+
)}
|
|
186
217
|
<td className="p-3">
|
|
187
218
|
<div className="flex flex-wrap gap-2">
|
|
188
219
|
{displayFacetValues.map(facetValue => (
|
|
@@ -192,7 +223,7 @@ export function AssignFacetValuesDialog({
|
|
|
192
223
|
removable={true}
|
|
193
224
|
onRemove={() =>
|
|
194
225
|
removeFacetValue(
|
|
195
|
-
|
|
226
|
+
entity.id,
|
|
196
227
|
facetValue.id,
|
|
197
228
|
)
|
|
198
229
|
}
|
|
@@ -14,24 +14,26 @@ import {
|
|
|
14
14
|
} from '@/components/ui/dialog.js';
|
|
15
15
|
import { Input } from '@/components/ui/input.js';
|
|
16
16
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select.js';
|
|
17
|
-
import { api } from '@/graphql/api.js';
|
|
18
17
|
import { ResultOf } from '@/graphql/graphql.js';
|
|
19
18
|
import { Trans, useLingui } from '@/lib/trans.js';
|
|
20
19
|
|
|
21
20
|
import { useChannel } from '@/hooks/use-channel.js';
|
|
22
|
-
import { assignProductsToChannelDocument } from '../products.graphql.js';
|
|
23
21
|
|
|
24
22
|
interface AssignToChannelDialogProps {
|
|
25
23
|
open: boolean;
|
|
26
24
|
onOpenChange: (open: boolean) => void;
|
|
27
|
-
|
|
25
|
+
entityIds: string[];
|
|
26
|
+
entityType: 'products' | 'variants';
|
|
27
|
+
mutationFn: (variables: any) => Promise<ResultOf<any>>;
|
|
28
28
|
onSuccess?: () => void;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function AssignToChannelDialog({
|
|
32
32
|
open,
|
|
33
33
|
onOpenChange,
|
|
34
|
-
|
|
34
|
+
entityIds,
|
|
35
|
+
entityType,
|
|
36
|
+
mutationFn,
|
|
35
37
|
onSuccess,
|
|
36
38
|
}: AssignToChannelDialogProps) {
|
|
37
39
|
const { i18n } = useLingui();
|
|
@@ -43,14 +45,14 @@ export function AssignToChannelDialog({
|
|
|
43
45
|
const availableChannels = channels.filter(channel => channel.id !== selectedChannel?.id);
|
|
44
46
|
|
|
45
47
|
const { mutate, isPending } = useMutation({
|
|
46
|
-
mutationFn
|
|
47
|
-
onSuccess: (
|
|
48
|
-
toast.success(i18n.t(`Successfully assigned ${
|
|
48
|
+
mutationFn,
|
|
49
|
+
onSuccess: () => {
|
|
50
|
+
toast.success(i18n.t(`Successfully assigned ${entityIds.length} ${entityType} to channel`));
|
|
49
51
|
onSuccess?.();
|
|
50
52
|
onOpenChange(false);
|
|
51
53
|
},
|
|
52
54
|
onError: () => {
|
|
53
|
-
toast.error(`Failed to assign ${
|
|
55
|
+
toast.error(`Failed to assign ${entityIds.length} ${entityType} to channel`);
|
|
54
56
|
},
|
|
55
57
|
});
|
|
56
58
|
|
|
@@ -60,13 +62,20 @@ export function AssignToChannelDialog({
|
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
|
|
78
|
+
mutate({ input });
|
|
70
79
|
};
|
|
71
80
|
|
|
72
81
|
return (
|
|
@@ -74,10 +83,12 @@ export function AssignToChannelDialog({
|
|
|
74
83
|
<DialogContent className="sm:max-w-[425px]">
|
|
75
84
|
<DialogHeader>
|
|
76
85
|
<DialogTitle>
|
|
77
|
-
<Trans>Assign
|
|
86
|
+
<Trans>Assign {entityType} to channel</Trans>
|
|
78
87
|
</DialogTitle>
|
|
79
88
|
<DialogDescription>
|
|
80
|
-
<Trans>
|
|
89
|
+
<Trans>
|
|
90
|
+
Select a channel to assign {entityIds.length} {entityType} to
|
|
91
|
+
</Trans>
|
|
81
92
|
</DialogDescription>
|
|
82
93
|
</DialogHeader>
|
|
83
94
|
<div className="grid gap-4 py-4">
|