@vendure/dashboard 3.3.6-master-202506290242 → 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
|
@@ -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">
|
|
@@ -12,9 +12,13 @@ import { Trans, useLingui } from '@/lib/trans.js';
|
|
|
12
12
|
|
|
13
13
|
import { Permission } from '@vendure/common/lib/generated-types';
|
|
14
14
|
import {
|
|
15
|
+
assignProductsToChannelDocument,
|
|
15
16
|
deleteProductsDocument,
|
|
16
17
|
duplicateEntityDocument,
|
|
18
|
+
getProductsWithFacetValuesByIdsDocument,
|
|
19
|
+
productDetailDocument,
|
|
17
20
|
removeProductsFromChannelDocument,
|
|
21
|
+
updateProductsDocument,
|
|
18
22
|
} from '../products.graphql.js';
|
|
19
23
|
import { AssignFacetValuesDialog } from './assign-facet-values-dialog.js';
|
|
20
24
|
import { AssignToChannelDialog } from './assign-to-channel-dialog.js';
|
|
@@ -84,7 +88,9 @@ export const AssignProductsToChannelBulkAction: BulkActionComponent<any> = ({ se
|
|
|
84
88
|
<AssignToChannelDialog
|
|
85
89
|
open={dialogOpen}
|
|
86
90
|
onOpenChange={setDialogOpen}
|
|
87
|
-
|
|
91
|
+
entityIds={selection.map(s => s.id)}
|
|
92
|
+
entityType="products"
|
|
93
|
+
mutationFn={api.mutate(assignProductsToChannelDocument)}
|
|
88
94
|
onSuccess={handleSuccess}
|
|
89
95
|
/>
|
|
90
96
|
</>
|
|
@@ -156,7 +162,11 @@ export const AssignFacetValuesToProductsBulkAction: BulkActionComponent<any> = (
|
|
|
156
162
|
<AssignFacetValuesDialog
|
|
157
163
|
open={dialogOpen}
|
|
158
164
|
onOpenChange={setDialogOpen}
|
|
159
|
-
|
|
165
|
+
entityIds={selection.map(s => s.id)}
|
|
166
|
+
entityType="products"
|
|
167
|
+
queryFn={variables => api.query(getProductsWithFacetValuesByIdsDocument, variables)}
|
|
168
|
+
mutationFn={api.mutate(updateProductsDocument)}
|
|
169
|
+
detailDocument={productDetailDocument}
|
|
160
170
|
onSuccess={handleSuccess}
|
|
161
171
|
/>
|
|
162
172
|
</>
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { Money } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import { useState } from "react";
|
|
8
|
-
import { productVariantListDocument } from "../products.graphql.js";
|
|
1
|
+
import { Money } from '@/components/data-display/money.js';
|
|
2
|
+
import {
|
|
3
|
+
PaginatedListDataTable,
|
|
4
|
+
PaginatedListRefresherRegisterFn,
|
|
5
|
+
} from '@/components/shared/paginated-list-data-table.js';
|
|
6
|
+
import { StockLevelLabel } from '@/components/shared/stock-level-label.js';
|
|
9
7
|
import { graphql } from '@/graphql/graphql.js';
|
|
8
|
+
import { useLocalFormat } from '@/hooks/use-local-format.js';
|
|
9
|
+
import { DetailPageButton } from '@/index.js';
|
|
10
|
+
import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { productVariantListDocument } from '../products.graphql.js';
|
|
10
13
|
|
|
11
14
|
export const deleteProductVariantDocument = graphql(`
|
|
12
15
|
mutation DeleteProductVariant($id: ID!) {
|
|
@@ -17,62 +20,78 @@ export const deleteProductVariantDocument = graphql(`
|
|
|
17
20
|
}
|
|
18
21
|
`);
|
|
19
22
|
|
|
20
|
-
|
|
21
23
|
interface ProductVariantsTableProps {
|
|
22
24
|
productId: string;
|
|
23
25
|
registerRefresher?: PaginatedListRefresherRegisterFn;
|
|
26
|
+
fromProductDetailPage?: boolean;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
export function ProductVariantsTable({
|
|
29
|
+
export function ProductVariantsTable({
|
|
30
|
+
productId,
|
|
31
|
+
registerRefresher,
|
|
32
|
+
fromProductDetailPage,
|
|
33
|
+
}: ProductVariantsTableProps) {
|
|
27
34
|
const { formatCurrencyName } = useLocalFormat();
|
|
28
35
|
const [page, setPage] = useState(1);
|
|
29
36
|
const [pageSize, setPageSize] = useState(10);
|
|
30
37
|
const [sorting, setSorting] = useState<SortingState>([]);
|
|
31
38
|
const [filters, setFilters] = useState<ColumnFiltersState>([]);
|
|
32
39
|
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
40
|
+
return (
|
|
41
|
+
<PaginatedListDataTable
|
|
42
|
+
registerRefresher={registerRefresher}
|
|
43
|
+
listQuery={productVariantListDocument}
|
|
44
|
+
deleteMutation={deleteProductVariantDocument}
|
|
45
|
+
transformVariables={variables => ({
|
|
46
|
+
...variables,
|
|
47
|
+
productId,
|
|
48
|
+
})}
|
|
49
|
+
defaultVisibility={{
|
|
50
|
+
id: false,
|
|
51
|
+
currencyCode: false,
|
|
52
|
+
}}
|
|
53
|
+
customizeColumns={{
|
|
54
|
+
name: {
|
|
55
|
+
header: 'Variant name',
|
|
56
|
+
cell: ({ row: { original } }) => (
|
|
57
|
+
<DetailPageButton
|
|
58
|
+
href={`../../product-variants/${original.id}`}
|
|
59
|
+
label={original.name}
|
|
60
|
+
search={fromProductDetailPage ? { from: 'product' } : undefined}
|
|
61
|
+
/>
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
currencyCode: {
|
|
65
|
+
cell: ({ row: { original } }) => formatCurrencyName(original.currencyCode, 'full'),
|
|
66
|
+
},
|
|
67
|
+
price: {
|
|
68
|
+
cell: ({ row: { original } }) => (
|
|
69
|
+
<Money value={original.price} currency={original.currencyCode} />
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
priceWithTax: {
|
|
73
|
+
cell: ({ row: { original } }) => (
|
|
74
|
+
<Money value={original.priceWithTax} currency={original.currencyCode} />
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
stockLevels: {
|
|
78
|
+
cell: ({ row: { original } }) => <StockLevelLabel stockLevels={original.stockLevels} />,
|
|
79
|
+
},
|
|
80
|
+
}}
|
|
81
|
+
page={page}
|
|
82
|
+
itemsPerPage={pageSize}
|
|
83
|
+
sorting={sorting}
|
|
84
|
+
columnFilters={filters}
|
|
85
|
+
onPageChange={(_, page, perPage) => {
|
|
86
|
+
setPage(page);
|
|
87
|
+
setPageSize(perPage);
|
|
88
|
+
}}
|
|
89
|
+
onSortChange={(_, sorting) => {
|
|
90
|
+
setSorting(sorting);
|
|
91
|
+
}}
|
|
92
|
+
onFilterChange={(_, filters) => {
|
|
93
|
+
setFilters(filters);
|
|
94
|
+
}}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
78
97
|
}
|
|
@@ -7,18 +7,20 @@ export function DetailPageButton({
|
|
|
7
7
|
href,
|
|
8
8
|
label,
|
|
9
9
|
disabled,
|
|
10
|
+
search,
|
|
10
11
|
}: {
|
|
11
12
|
label: string | React.ReactNode;
|
|
12
13
|
id?: string;
|
|
13
14
|
href?: string;
|
|
14
15
|
disabled?: boolean;
|
|
16
|
+
search?: Record<string, string>;
|
|
15
17
|
}) {
|
|
16
18
|
if (!id && !href) {
|
|
17
19
|
return <span>{label}</span>;
|
|
18
20
|
}
|
|
19
21
|
return (
|
|
20
22
|
<Button asChild variant="ghost" disabled={disabled}>
|
|
21
|
-
<Link to={href ?? `./${id}`}>
|
|
23
|
+
<Link to={href ?? `./${id}`} search={search ?? {}}>
|
|
22
24
|
{label}
|
|
23
25
|
{!disabled && <ChevronRight className="h-3 w-3 text-muted-foreground" />}
|
|
24
26
|
</Link>
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import { DisplayComponent } from '@/framework/component-registry/dynamic-component.js';
|
|
31
31
|
import { BulkAction } from '@/framework/data-table/data-table-types.js';
|
|
32
32
|
import { ResultOf } from '@/graphql/graphql.js';
|
|
33
|
+
import { useExtendedListQuery } from '@/hooks/use-extended-list-query.js';
|
|
33
34
|
import { Trans, useLingui } from '@/lib/trans.js';
|
|
34
35
|
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
35
36
|
import {
|
|
@@ -276,6 +277,7 @@ export function PaginatedListDataTable<
|
|
|
276
277
|
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
|
277
278
|
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
|
278
279
|
const queryClient = useQueryClient();
|
|
280
|
+
const extendedListQuery = useExtendedListQuery(listQuery);
|
|
279
281
|
|
|
280
282
|
const sort = sorting?.reduce((acc: any, sort: ColumnSort) => {
|
|
281
283
|
const direction = sort.desc ? 'DESC' : 'ASC';
|
|
@@ -300,7 +302,7 @@ export function PaginatedListDataTable<
|
|
|
300
302
|
|
|
301
303
|
const defaultQueryKey = [
|
|
302
304
|
PaginatedListDataTableKey,
|
|
303
|
-
|
|
305
|
+
extendedListQuery,
|
|
304
306
|
page,
|
|
305
307
|
itemsPerPage,
|
|
306
308
|
sorting,
|
|
@@ -329,14 +331,14 @@ export function PaginatedListDataTable<
|
|
|
329
331
|
} as V;
|
|
330
332
|
|
|
331
333
|
const transformedVariables = transformVariables ? transformVariables(variables) : variables;
|
|
332
|
-
return api.query(
|
|
334
|
+
return api.query(extendedListQuery, transformedVariables);
|
|
333
335
|
},
|
|
334
336
|
queryKey,
|
|
335
337
|
placeholderData: keepPreviousData,
|
|
336
338
|
});
|
|
337
339
|
|
|
338
|
-
const fields = useListQueryFields(
|
|
339
|
-
const paginatedListObjectPath = getObjectPathToPaginatedList(
|
|
340
|
+
const fields = useListQueryFields(extendedListQuery);
|
|
341
|
+
const paginatedListObjectPath = getObjectPathToPaginatedList(extendedListQuery);
|
|
340
342
|
|
|
341
343
|
let listData = data as any;
|
|
342
344
|
for (const path of paginatedListObjectPath) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { BulkAction } from '@/framework/data-table/data-table-types.js';
|
|
2
|
+
import { DocumentNode } from 'graphql';
|
|
2
3
|
|
|
3
4
|
import { globalRegistry } from '../registry/global-registry.js';
|
|
4
5
|
|
|
5
6
|
globalRegistry.register('bulkActionsRegistry', new Map<string, BulkAction[]>());
|
|
7
|
+
globalRegistry.register('listQueryDocumentRegistry', new Map<string, DocumentNode[]>());
|
|
6
8
|
|
|
7
9
|
export function getBulkActions(pageId: string, blockId = 'list-table'): BulkAction[] {
|
|
8
10
|
const key = createKey(pageId, blockId);
|
|
@@ -16,6 +18,18 @@ export function addBulkAction(pageId: string, blockId: string | undefined, actio
|
|
|
16
18
|
bulkActionsRegistry.set(key, [...existingActions, action]);
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
export function getListQueryDocuments(pageId: string, blockId = 'list-table'): DocumentNode[] {
|
|
22
|
+
const key = createKey(pageId, blockId);
|
|
23
|
+
return globalRegistry.get('listQueryDocumentRegistry').get(key) || [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function addListQueryDocument(pageId: string, blockId: string | undefined, document: DocumentNode) {
|
|
27
|
+
const listQueryDocumentRegistry = globalRegistry.get('listQueryDocumentRegistry');
|
|
28
|
+
const key = createKey(pageId, blockId);
|
|
29
|
+
const existingDocuments = listQueryDocumentRegistry.get(key) || [];
|
|
30
|
+
listQueryDocumentRegistry.set(key, [...existingDocuments, document]);
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
function createKey(pageId: string, blockId: string | undefined): string {
|
|
20
34
|
return `${pageId}__${blockId ?? 'list-table'}`;
|
|
21
35
|
}
|