@vendure/dashboard 3.4.3-master-202509260228 → 3.5.0-minor-202509261210
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin/api/api-extensions.js +11 -14
- package/dist/plugin/api/metrics.resolver.d.ts +2 -2
- package/dist/plugin/api/metrics.resolver.js +2 -2
- package/dist/plugin/config/metrics-strategies.d.ts +9 -9
- package/dist/plugin/config/metrics-strategies.js +6 -6
- package/dist/plugin/constants.d.ts +2 -0
- package/dist/plugin/constants.js +3 -1
- package/dist/plugin/dashboard.plugin.js +13 -0
- package/dist/plugin/service/metrics.service.d.ts +3 -3
- package/dist/plugin/service/metrics.service.js +37 -53
- package/dist/plugin/types.d.ts +9 -12
- package/dist/plugin/types.js +7 -11
- package/dist/vite/vite-plugin-vendure-dashboard.js +2 -2
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_collections/collections.tsx +7 -2
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +15 -2
- package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +14 -2
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +10 -0
- package/src/app/routes/_authenticated/_products/components/product-option-group-badge.tsx +19 -0
- package/src/app/routes/_authenticated/_products/components/product-options-table.tsx +111 -0
- package/src/app/routes/_authenticated/_products/product-option-groups.graphql.ts +103 -0
- package/src/app/routes/_authenticated/_products/products.graphql.ts +13 -1
- package/src/app/routes/_authenticated/_products/products.tsx +27 -3
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +26 -9
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +181 -0
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +208 -0
- package/src/app/routes/_authenticated/_zones/components/zone-countries-sheet.tsx +4 -1
- package/src/app/routes/_authenticated/index.tsx +41 -24
- package/src/lib/components/data-display/json.tsx +16 -1
- package/src/lib/components/data-input/index.ts +3 -0
- package/src/lib/components/data-input/slug-input.tsx +296 -0
- package/src/lib/components/data-table/add-filter-menu.tsx +13 -6
- package/src/lib/components/data-table/data-table-bulk-action-item.tsx +38 -1
- package/src/lib/components/data-table/data-table-context.tsx +91 -0
- package/src/lib/components/data-table/data-table-filter-badge.tsx +9 -5
- package/src/lib/components/data-table/data-table-view-options.tsx +17 -8
- package/src/lib/components/data-table/data-table.tsx +146 -94
- package/src/lib/components/data-table/global-views-bar.tsx +97 -0
- package/src/lib/components/data-table/global-views-sheet.tsx +11 -0
- package/src/lib/components/data-table/manage-global-views-button.tsx +26 -0
- package/src/lib/components/data-table/my-views-button.tsx +47 -0
- package/src/lib/components/data-table/refresh-button.tsx +12 -3
- package/src/lib/components/data-table/save-view-button.tsx +45 -0
- package/src/lib/components/data-table/save-view-dialog.tsx +113 -0
- package/src/lib/components/data-table/use-generated-columns.tsx +3 -1
- package/src/lib/components/data-table/user-views-sheet.tsx +11 -0
- package/src/lib/components/data-table/views-sheet.tsx +297 -0
- package/src/lib/components/date-range-picker.tsx +184 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +59 -32
- package/src/lib/components/ui/button.tsx +1 -1
- package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +29 -2
- package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +10 -7
- package/src/lib/framework/dashboard-widget/metrics-widget/metrics-widget.graphql.ts +9 -3
- package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +19 -75
- package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +33 -0
- package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +319 -9
- package/src/lib/framework/document-introspection/add-custom-fields.ts +60 -31
- package/src/lib/framework/document-introspection/get-document-structure.spec.ts +1 -159
- package/src/lib/framework/document-introspection/include-only-selected-list-fields.spec.ts +1840 -0
- package/src/lib/framework/document-introspection/include-only-selected-list-fields.ts +940 -0
- package/src/lib/framework/document-introspection/testing-utils.ts +161 -0
- package/src/lib/framework/extension-api/display-component-extensions.tsx +2 -0
- package/src/lib/framework/extension-api/types/data-table.ts +62 -4
- package/src/lib/framework/extension-api/types/navigation.ts +16 -0
- package/src/lib/framework/form-engine/utils.ts +34 -0
- package/src/lib/framework/page/list-page.tsx +289 -4
- package/src/lib/framework/page/use-extended-router.tsx +59 -17
- package/src/lib/graphql/api.ts +4 -2
- package/src/lib/graphql/graphql-env.d.ts +13 -10
- package/src/lib/hooks/use-extended-list-query.ts +5 -0
- package/src/lib/hooks/use-saved-views.ts +230 -0
- package/src/lib/index.ts +15 -0
- package/src/lib/types/saved-views.ts +39 -0
- package/src/lib/utils/saved-views-utils.ts +40 -0
|
@@ -9,13 +9,15 @@ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
|
9
9
|
import { Link } from '@tanstack/react-router';
|
|
10
10
|
import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
|
|
11
11
|
import { formatRelative } from 'date-fns';
|
|
12
|
-
import { useState } from 'react';
|
|
12
|
+
import { useEffect, useState } from 'react';
|
|
13
13
|
import { DashboardBaseWidget } from '../base-widget.js';
|
|
14
|
+
import { useWidgetFilters } from '../widget-filters-context.js';
|
|
14
15
|
import { latestOrdersQuery } from './latest-orders-widget.graphql.js';
|
|
15
16
|
|
|
16
17
|
export const WIDGET_ID = 'latest-orders-widget';
|
|
17
18
|
|
|
18
19
|
export function LatestOrdersWidget() {
|
|
20
|
+
const { dateRange } = useWidgetFilters();
|
|
19
21
|
const [sorting, setSorting] = useState<SortingState>([
|
|
20
22
|
{
|
|
21
23
|
id: 'orderPlacedAt',
|
|
@@ -24,9 +26,34 @@ export function LatestOrdersWidget() {
|
|
|
24
26
|
]);
|
|
25
27
|
const [page, setPage] = useState(1);
|
|
26
28
|
const [pageSize, setPageSize] = useState(10);
|
|
27
|
-
const [filters, setFilters] = useState<ColumnFiltersState>([
|
|
29
|
+
const [filters, setFilters] = useState<ColumnFiltersState>([
|
|
30
|
+
{
|
|
31
|
+
id: 'orderPlacedAt',
|
|
32
|
+
value: {
|
|
33
|
+
between: {
|
|
34
|
+
start: dateRange.from.toISOString(),
|
|
35
|
+
end: dateRange.to.toISOString(),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
28
40
|
const { formatCurrency } = useLocalFormat();
|
|
29
41
|
|
|
42
|
+
// Update filters when date range changes
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
setFilters([
|
|
45
|
+
{
|
|
46
|
+
id: 'orderPlacedAt',
|
|
47
|
+
value: {
|
|
48
|
+
between: {
|
|
49
|
+
start: dateRange.from.toISOString(),
|
|
50
|
+
end: dateRange.to.toISOString(),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
}, [dateRange]);
|
|
56
|
+
|
|
30
57
|
return (
|
|
31
58
|
<DashboardBaseWidget id={WIDGET_ID} title="Latest Orders" description="Your latest orders">
|
|
32
59
|
<PaginatedListDataTable
|
|
@@ -7,6 +7,7 @@ import { useQuery } from '@tanstack/react-query';
|
|
|
7
7
|
import { RefreshCw } from 'lucide-react';
|
|
8
8
|
import { useMemo, useState } from 'react';
|
|
9
9
|
import { DashboardBaseWidget } from '../base-widget.js';
|
|
10
|
+
import { useWidgetFilters } from '../widget-filters-context.js';
|
|
10
11
|
import { MetricsChart } from './chart.js';
|
|
11
12
|
import { orderChartDataQuery } from './metrics-widget.graphql.js';
|
|
12
13
|
|
|
@@ -19,37 +20,39 @@ enum DATA_TYPES {
|
|
|
19
20
|
export function MetricsWidget() {
|
|
20
21
|
const { formatDate, formatCurrency } = useLocalFormat();
|
|
21
22
|
const { activeChannel } = useChannel();
|
|
23
|
+
const { dateRange } = useWidgetFilters();
|
|
22
24
|
const [dataType, setDataType] = useState<DATA_TYPES>(DATA_TYPES.OrderTotal);
|
|
23
25
|
|
|
24
|
-
const { data,
|
|
25
|
-
queryKey: ['dashboard-order-metrics', dataType],
|
|
26
|
+
const { data, refetch, isRefetching } = useQuery({
|
|
27
|
+
queryKey: ['dashboard-order-metrics', dataType, dateRange],
|
|
26
28
|
queryFn: () => {
|
|
27
29
|
return api.query(orderChartDataQuery, {
|
|
28
30
|
types: [dataType],
|
|
29
31
|
refresh: true,
|
|
32
|
+
startDate: dateRange.from.toISOString(),
|
|
33
|
+
endDate: dateRange.to.toISOString(),
|
|
30
34
|
});
|
|
31
35
|
},
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
const chartData = useMemo(() => {
|
|
35
|
-
const entry = data?.
|
|
39
|
+
const entry = data?.dashboardMetricSummary.at(0);
|
|
36
40
|
if (!entry) {
|
|
37
41
|
return undefined;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
const {
|
|
44
|
+
const { type, entries } = entry;
|
|
41
45
|
|
|
42
|
-
const values = entries.map(({ label, value }) => ({
|
|
46
|
+
const values = entries.map(({ label, value }: { label: string; value: number }) => ({
|
|
43
47
|
name: formatDate(label, { month: 'short', day: 'numeric' }),
|
|
44
48
|
sales: value,
|
|
45
49
|
}));
|
|
46
50
|
|
|
47
51
|
return {
|
|
48
52
|
values,
|
|
49
|
-
interval,
|
|
50
53
|
type,
|
|
51
54
|
};
|
|
52
|
-
}, [data]);
|
|
55
|
+
}, [data, formatDate]);
|
|
53
56
|
|
|
54
57
|
return (
|
|
55
58
|
<DashboardBaseWidget
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { graphql } from '@/vdb/graphql/graphql.js';
|
|
2
2
|
|
|
3
3
|
export const orderChartDataQuery = graphql(`
|
|
4
|
-
query GetOrderChartData(
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
query GetOrderChartData(
|
|
5
|
+
$refresh: Boolean
|
|
6
|
+
$types: [DashboardMetricType!]!
|
|
7
|
+
$startDate: DateTime!
|
|
8
|
+
$endDate: DateTime!
|
|
9
|
+
) {
|
|
10
|
+
dashboardMetricSummary(
|
|
11
|
+
input: { types: $types, refresh: $refresh, startDate: $startDate, endDate: $endDate }
|
|
12
|
+
) {
|
|
7
13
|
type
|
|
8
14
|
entries {
|
|
9
15
|
label
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import { AnimatedCurrency, AnimatedNumber } from '@/vdb/components/shared/animated-number.js';
|
|
2
|
-
import { Tabs, TabsList, TabsTrigger } from '@/vdb/components/ui/tabs.js';
|
|
3
2
|
import { api } from '@/vdb/graphql/api.js';
|
|
4
3
|
import { useQuery } from '@tanstack/react-query';
|
|
5
|
-
import {
|
|
6
|
-
import { useMemo
|
|
4
|
+
import { differenceInDays, subDays } from 'date-fns';
|
|
5
|
+
import { useMemo } from 'react';
|
|
7
6
|
import { DashboardBaseWidget } from '../base-widget.js';
|
|
7
|
+
import { useWidgetFilters } from '../widget-filters-context.js';
|
|
8
8
|
import { orderSummaryQuery } from './order-summary-widget.graphql.js';
|
|
9
9
|
|
|
10
10
|
const WIDGET_ID = 'orders-summary-widget';
|
|
11
11
|
|
|
12
|
-
enum Range {
|
|
13
|
-
Today = 'today',
|
|
14
|
-
Yesterday = 'yesterday',
|
|
15
|
-
ThisWeek = 'thisWeek',
|
|
16
|
-
ThisMonth = 'thisMonth',
|
|
17
|
-
}
|
|
18
|
-
|
|
19
12
|
interface PercentageChangeProps {
|
|
20
13
|
value: number;
|
|
21
14
|
}
|
|
@@ -34,63 +27,24 @@ function PercentageChange({ value }: PercentageChangeProps) {
|
|
|
34
27
|
}
|
|
35
28
|
|
|
36
29
|
export function OrdersSummaryWidget() {
|
|
37
|
-
const
|
|
30
|
+
const { dateRange } = useWidgetFilters();
|
|
38
31
|
|
|
39
32
|
const variables = useMemo(() => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
case Range.Yesterday: {
|
|
55
|
-
const yesterday = subDays(now, 1);
|
|
56
|
-
const dayBeforeYesterday = subDays(now, 2);
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
start: startOfDay(yesterday).toISOString(),
|
|
60
|
-
end: endOfDay(yesterday).toISOString(),
|
|
61
|
-
previousStart: startOfDay(dayBeforeYesterday).toISOString(),
|
|
62
|
-
previousEnd: endOfDay(dayBeforeYesterday).toISOString(),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
case Range.ThisWeek: {
|
|
66
|
-
const today = now;
|
|
67
|
-
const sixDaysAgo = subDays(now, 6);
|
|
68
|
-
const sevenDaysAgo = subDays(now, 7);
|
|
69
|
-
const thirteenDaysAgo = subDays(now, 13);
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
start: startOfDay(sixDaysAgo).toISOString(),
|
|
73
|
-
end: endOfDay(today).toISOString(),
|
|
74
|
-
previousStart: startOfDay(thirteenDaysAgo).toISOString(),
|
|
75
|
-
previousEnd: endOfDay(sevenDaysAgo).toISOString(),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
case Range.ThisMonth: {
|
|
79
|
-
const lastMonth = subMonths(now, 1);
|
|
80
|
-
const twoMonthsAgo = subMonths(now, 2);
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
start: startOfMonth(lastMonth).toISOString(),
|
|
84
|
-
end: endOfMonth(lastMonth).toISOString(),
|
|
85
|
-
previousStart: startOfMonth(twoMonthsAgo).toISOString(),
|
|
86
|
-
previousEnd: endOfMonth(twoMonthsAgo).toISOString(),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}, [range]);
|
|
33
|
+
const rangeLength = differenceInDays(dateRange.to, dateRange.from) + 1;
|
|
34
|
+
// For the previous period, we go back by the same range length
|
|
35
|
+
const previousStart = subDays(dateRange.from, rangeLength);
|
|
36
|
+
const previousEnd = subDays(dateRange.to, rangeLength);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
start: dateRange.from.toISOString(),
|
|
40
|
+
end: dateRange.to.toISOString(),
|
|
41
|
+
previousStart: previousStart.toISOString(),
|
|
42
|
+
previousEnd: previousEnd.toISOString(),
|
|
43
|
+
};
|
|
44
|
+
}, [dateRange]);
|
|
91
45
|
|
|
92
46
|
const { data } = useQuery({
|
|
93
|
-
queryKey: ['orders-summary',
|
|
47
|
+
queryKey: ['orders-summary', dateRange],
|
|
94
48
|
queryFn: () =>
|
|
95
49
|
api.query(orderSummaryQuery, {
|
|
96
50
|
start: variables.start,
|
|
@@ -99,7 +53,7 @@ export function OrdersSummaryWidget() {
|
|
|
99
53
|
});
|
|
100
54
|
|
|
101
55
|
const { data: previousData } = useQuery({
|
|
102
|
-
queryKey: ['orders-summary', 'previous',
|
|
56
|
+
queryKey: ['orders-summary', 'previous', dateRange],
|
|
103
57
|
queryFn: () =>
|
|
104
58
|
api.query(orderSummaryQuery, {
|
|
105
59
|
start: variables.previousStart,
|
|
@@ -126,16 +80,6 @@ export function OrdersSummaryWidget() {
|
|
|
126
80
|
id={WIDGET_ID}
|
|
127
81
|
title="Orders Summary"
|
|
128
82
|
description="Your orders summary"
|
|
129
|
-
actions={
|
|
130
|
-
<Tabs defaultValue={range} onValueChange={value => setRange(value as Range)}>
|
|
131
|
-
<TabsList>
|
|
132
|
-
<TabsTrigger value={Range.Today}>Today</TabsTrigger>
|
|
133
|
-
<TabsTrigger value={Range.Yesterday}>Yesterday</TabsTrigger>
|
|
134
|
-
<TabsTrigger value={Range.ThisWeek}>This Week</TabsTrigger>
|
|
135
|
-
<TabsTrigger value={Range.ThisMonth}>This Month</TabsTrigger>
|
|
136
|
-
</TabsList>
|
|
137
|
-
</Tabs>
|
|
138
|
-
}
|
|
139
83
|
>
|
|
140
84
|
<div className="@container h-full">
|
|
141
85
|
<div className="flex flex-col h-full @md:flex-row gap-8 items-center justify-center @md:justify-evenly text-center tabular-nums">
|
|
@@ -163,4 +107,4 @@ export function OrdersSummaryWidget() {
|
|
|
163
107
|
</div>
|
|
164
108
|
</DashboardBaseWidget>
|
|
165
109
|
);
|
|
166
|
-
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, PropsWithChildren } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface DefinedDateRange {
|
|
6
|
+
from: Date;
|
|
7
|
+
to: Date;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface WidgetFilters {
|
|
11
|
+
dateRange: DefinedDateRange;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const WidgetFiltersContext = createContext<WidgetFilters | undefined>(undefined);
|
|
15
|
+
|
|
16
|
+
export function WidgetFiltersProvider({
|
|
17
|
+
children,
|
|
18
|
+
filters
|
|
19
|
+
}: PropsWithChildren<{ filters: WidgetFilters }>) {
|
|
20
|
+
return (
|
|
21
|
+
<WidgetFiltersContext.Provider value={filters}>
|
|
22
|
+
{children}
|
|
23
|
+
</WidgetFiltersContext.Provider>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useWidgetFilters() {
|
|
28
|
+
const context = useContext(WidgetFiltersContext);
|
|
29
|
+
if (context === undefined) {
|
|
30
|
+
throw new Error('useWidgetFilters must be used within a WidgetFiltersProvider');
|
|
31
|
+
}
|
|
32
|
+
return context;
|
|
33
|
+
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { CustomFieldConfig, CustomFields } from '@vendure/common/lib/generated-types';
|
|
2
2
|
import { graphql } from 'gql.tada';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
FieldNode,
|
|
6
|
-
FragmentDefinitionNode,
|
|
7
|
-
Kind,
|
|
8
|
-
OperationDefinitionNode,
|
|
9
|
-
print,
|
|
10
|
-
} from 'graphql';
|
|
11
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
3
|
+
import { DocumentNode, FieldNode, FragmentDefinitionNode, Kind, print } from 'graphql';
|
|
4
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
12
5
|
|
|
13
6
|
import { addCustomFields } from './add-custom-fields.js';
|
|
14
7
|
|
|
@@ -239,4 +232,321 @@ describe('addCustomFields()', () => {
|
|
|
239
232
|
addsCustomFieldsToType('Address', addressFragment);
|
|
240
233
|
});
|
|
241
234
|
});
|
|
235
|
+
|
|
236
|
+
describe('Nested entity handling', () => {
|
|
237
|
+
it('User example: Should not add custom fields to Asset fragment used in nested featuredAsset', () => {
|
|
238
|
+
const assetFragment = graphql(`
|
|
239
|
+
fragment Asset on Asset {
|
|
240
|
+
id
|
|
241
|
+
createdAt
|
|
242
|
+
updatedAt
|
|
243
|
+
name
|
|
244
|
+
fileSize
|
|
245
|
+
mimeType
|
|
246
|
+
type
|
|
247
|
+
preview
|
|
248
|
+
source
|
|
249
|
+
width
|
|
250
|
+
height
|
|
251
|
+
focalPoint {
|
|
252
|
+
x
|
|
253
|
+
y
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
`);
|
|
257
|
+
|
|
258
|
+
const documentNode = graphql(
|
|
259
|
+
`
|
|
260
|
+
query CollectionList($options: CollectionListOptions) {
|
|
261
|
+
collections(options: $options) {
|
|
262
|
+
items {
|
|
263
|
+
id
|
|
264
|
+
createdAt
|
|
265
|
+
updatedAt
|
|
266
|
+
featuredAsset {
|
|
267
|
+
...Asset
|
|
268
|
+
}
|
|
269
|
+
name
|
|
270
|
+
slug
|
|
271
|
+
breadcrumbs {
|
|
272
|
+
id
|
|
273
|
+
name
|
|
274
|
+
slug
|
|
275
|
+
}
|
|
276
|
+
children {
|
|
277
|
+
id
|
|
278
|
+
name
|
|
279
|
+
}
|
|
280
|
+
position
|
|
281
|
+
isPrivate
|
|
282
|
+
parentId
|
|
283
|
+
productVariants {
|
|
284
|
+
totalItems
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
totalItems
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
`,
|
|
291
|
+
[assetFragment],
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
|
|
295
|
+
customFieldsConfig.set('Collection', [
|
|
296
|
+
{ name: 'featuredProducts', type: 'relation', list: true, scalarFields: ['id', 'name'] },
|
|
297
|
+
{ name: 'alternativeAsset', type: 'relation', list: false, scalarFields: ['id', 'name'] },
|
|
298
|
+
]);
|
|
299
|
+
customFieldsConfig.set('Asset', [
|
|
300
|
+
{ name: 'assetCustomField1', type: 'string', list: false },
|
|
301
|
+
{ name: 'assetCustomField2', type: 'string', list: false },
|
|
302
|
+
{ name: 'assetCustomField3', type: 'string', list: false },
|
|
303
|
+
]);
|
|
304
|
+
|
|
305
|
+
const result = addCustomFields(documentNode, { customFieldsMap: customFieldsConfig });
|
|
306
|
+
const printed = print(result);
|
|
307
|
+
|
|
308
|
+
// Should add customFields ONLY to Collection items (top-level entity)
|
|
309
|
+
expect(printed).toContain('customFields {');
|
|
310
|
+
expect(printed).toContain('featuredProducts {');
|
|
311
|
+
expect(printed).toContain('alternativeAsset {');
|
|
312
|
+
|
|
313
|
+
// Should NOT add customFields to Asset fragment (only used in nested context)
|
|
314
|
+
const fragmentMatch = printed.match(/fragment Asset on Asset\s*\{[^}]*\}/s);
|
|
315
|
+
expect(fragmentMatch).toBeTruthy();
|
|
316
|
+
expect(fragmentMatch![0]).not.toContain('customFields');
|
|
317
|
+
expect(fragmentMatch![0]).not.toContain('assetCustomField1');
|
|
318
|
+
|
|
319
|
+
// Should NOT add customFields to children (nested Collection entities)
|
|
320
|
+
const childrenMatch = printed.match(/children\s*\{[^}]+\}/s);
|
|
321
|
+
expect(childrenMatch).toBeTruthy();
|
|
322
|
+
expect(childrenMatch![0]).not.toContain('customFields');
|
|
323
|
+
|
|
324
|
+
// Should NOT add customFields to breadcrumbs
|
|
325
|
+
const breadcrumbsMatch = printed.match(/breadcrumbs\s*\{[^}]+\}/s);
|
|
326
|
+
expect(breadcrumbsMatch).toBeTruthy();
|
|
327
|
+
expect(breadcrumbsMatch![0]).not.toContain('customFields');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('Should only add custom fields to top-level entity, not nested related entities', () => {
|
|
331
|
+
const documentNode = graphql(`
|
|
332
|
+
query CollectionList($options: CollectionListOptions) {
|
|
333
|
+
collections(options: $options) {
|
|
334
|
+
items {
|
|
335
|
+
id
|
|
336
|
+
name
|
|
337
|
+
slug
|
|
338
|
+
featuredAsset {
|
|
339
|
+
id
|
|
340
|
+
preview
|
|
341
|
+
}
|
|
342
|
+
children {
|
|
343
|
+
id
|
|
344
|
+
name
|
|
345
|
+
slug
|
|
346
|
+
}
|
|
347
|
+
breadcrumbs {
|
|
348
|
+
id
|
|
349
|
+
name
|
|
350
|
+
slug
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
totalItems
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
`);
|
|
357
|
+
|
|
358
|
+
const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
|
|
359
|
+
customFieldsConfig.set('Collection', [
|
|
360
|
+
{ name: 'featuredProducts', type: 'relation', list: true, scalarFields: ['id', 'name'] },
|
|
361
|
+
{ name: 'alternativeAsset', type: 'relation', list: false, scalarFields: ['id', 'name'] },
|
|
362
|
+
]);
|
|
363
|
+
customFieldsConfig.set('Asset', [
|
|
364
|
+
{ name: 'assetCustomField1', type: 'string', list: false },
|
|
365
|
+
{ name: 'assetCustomField2', type: 'string', list: false },
|
|
366
|
+
]);
|
|
367
|
+
|
|
368
|
+
const result = addCustomFields(documentNode, { customFieldsMap: customFieldsConfig });
|
|
369
|
+
const printed = print(result);
|
|
370
|
+
|
|
371
|
+
// Should add customFields to top-level Collection (items)
|
|
372
|
+
expect(printed).toContain('customFields {');
|
|
373
|
+
expect(printed).toContain('featuredProducts {');
|
|
374
|
+
expect(printed).toContain('alternativeAsset {');
|
|
375
|
+
|
|
376
|
+
// Should NOT add customFields to nested Collection entities (children, breadcrumbs)
|
|
377
|
+
const childrenMatch = printed.match(/children\s*\{[^}]+\}/s);
|
|
378
|
+
const breadcrumbsMatch = printed.match(/breadcrumbs\s*\{[^}]+\}/s);
|
|
379
|
+
|
|
380
|
+
expect(childrenMatch).toBeTruthy();
|
|
381
|
+
expect(breadcrumbsMatch).toBeTruthy();
|
|
382
|
+
expect(childrenMatch![0]).not.toContain('customFields');
|
|
383
|
+
expect(breadcrumbsMatch![0]).not.toContain('customFields');
|
|
384
|
+
|
|
385
|
+
// Should NOT add customFields to nested Asset entity (featuredAsset)
|
|
386
|
+
const featuredAssetMatch = printed.match(/featuredAsset\s*\{[^}]+\}/s);
|
|
387
|
+
expect(featuredAssetMatch).toBeTruthy();
|
|
388
|
+
expect(featuredAssetMatch![0]).not.toContain('customFields');
|
|
389
|
+
expect(featuredAssetMatch![0]).not.toContain('assetCustomField1');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('Should NOT add custom fields to fragments that are only used in nested contexts', () => {
|
|
393
|
+
const assetFragment = graphql(`
|
|
394
|
+
fragment Asset on Asset {
|
|
395
|
+
id
|
|
396
|
+
name
|
|
397
|
+
preview
|
|
398
|
+
}
|
|
399
|
+
`);
|
|
400
|
+
|
|
401
|
+
const documentNode = graphql(
|
|
402
|
+
`
|
|
403
|
+
query ProductList($options: ProductListOptions) {
|
|
404
|
+
products(options: $options) {
|
|
405
|
+
items {
|
|
406
|
+
id
|
|
407
|
+
name
|
|
408
|
+
featuredAsset {
|
|
409
|
+
...Asset
|
|
410
|
+
}
|
|
411
|
+
variants {
|
|
412
|
+
id
|
|
413
|
+
name
|
|
414
|
+
assets {
|
|
415
|
+
...Asset
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
totalItems
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
`,
|
|
423
|
+
[assetFragment],
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
|
|
427
|
+
customFieldsConfig.set('Product', [{ name: 'productCustomField', type: 'string', list: false }]);
|
|
428
|
+
customFieldsConfig.set('Asset', [{ name: 'assetCustomField', type: 'string', list: false }]);
|
|
429
|
+
customFieldsConfig.set('ProductVariant', [
|
|
430
|
+
{ name: 'variantCustomField', type: 'string', list: false },
|
|
431
|
+
]);
|
|
432
|
+
|
|
433
|
+
const result = addCustomFields(documentNode, { customFieldsMap: customFieldsConfig });
|
|
434
|
+
const printed = print(result);
|
|
435
|
+
|
|
436
|
+
// Should add customFields to Product (top-level query entity)
|
|
437
|
+
expect(printed).toContain('customFields {');
|
|
438
|
+
expect(printed).toContain('productCustomField');
|
|
439
|
+
|
|
440
|
+
// Should NOT add customFields to Asset fragment (only used in nested contexts)
|
|
441
|
+
expect(printed).not.toMatch(/fragment Asset on Asset\s*\{[^}]*customFields/s);
|
|
442
|
+
|
|
443
|
+
// Should NOT add customFields to nested ProductVariant entities
|
|
444
|
+
const variantsMatch = printed.match(/variants\s*\{[^}]+\}/s);
|
|
445
|
+
expect(variantsMatch).toBeTruthy();
|
|
446
|
+
expect(variantsMatch![0]).not.toContain('customFields');
|
|
447
|
+
expect(variantsMatch![0]).not.toContain('variantCustomField');
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('Should add custom fields to fragments used at top level', () => {
|
|
451
|
+
const productFragment = graphql(`
|
|
452
|
+
fragment ProductDetails on Product {
|
|
453
|
+
id
|
|
454
|
+
name
|
|
455
|
+
slug
|
|
456
|
+
}
|
|
457
|
+
`);
|
|
458
|
+
|
|
459
|
+
const documentNode = graphql(
|
|
460
|
+
`
|
|
461
|
+
query ProductList($options: ProductListOptions) {
|
|
462
|
+
products(options: $options) {
|
|
463
|
+
items {
|
|
464
|
+
...ProductDetails
|
|
465
|
+
featuredAsset {
|
|
466
|
+
id
|
|
467
|
+
preview
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
totalItems
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
`,
|
|
474
|
+
[productFragment],
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
|
|
478
|
+
customFieldsConfig.set('Product', [{ name: 'productCustomField', type: 'string', list: false }]);
|
|
479
|
+
customFieldsConfig.set('Asset', [{ name: 'assetCustomField', type: 'string', list: false }]);
|
|
480
|
+
|
|
481
|
+
const result = addCustomFields(documentNode, { customFieldsMap: customFieldsConfig });
|
|
482
|
+
const printed = print(result);
|
|
483
|
+
|
|
484
|
+
// Should add customFields to ProductDetails fragment (used at top level in items)
|
|
485
|
+
expect(printed).toContain('fragment ProductDetails on Product');
|
|
486
|
+
expect(printed).toContain('productCustomField');
|
|
487
|
+
|
|
488
|
+
// Should NOT add customFields to featuredAsset (nested entity)
|
|
489
|
+
const featuredAssetMatch = printed.match(/featuredAsset\s*\{[^}]+\}/s);
|
|
490
|
+
expect(featuredAssetMatch).toBeTruthy();
|
|
491
|
+
expect(featuredAssetMatch![0]).not.toContain('customFields');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('Should handle complex nested structure with multiple entity types', () => {
|
|
495
|
+
const documentNode = graphql(`
|
|
496
|
+
query ComplexQuery {
|
|
497
|
+
orders {
|
|
498
|
+
items {
|
|
499
|
+
id
|
|
500
|
+
code
|
|
501
|
+
customer {
|
|
502
|
+
id
|
|
503
|
+
firstName
|
|
504
|
+
addresses {
|
|
505
|
+
id
|
|
506
|
+
streetLine1
|
|
507
|
+
country {
|
|
508
|
+
id
|
|
509
|
+
name
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
lines {
|
|
514
|
+
id
|
|
515
|
+
productVariant {
|
|
516
|
+
id
|
|
517
|
+
name
|
|
518
|
+
product {
|
|
519
|
+
id
|
|
520
|
+
name
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
`);
|
|
528
|
+
|
|
529
|
+
const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
|
|
530
|
+
customFieldsConfig.set('Order', [{ name: 'orderCustomField', type: 'string', list: false }]);
|
|
531
|
+
customFieldsConfig.set('Customer', [
|
|
532
|
+
{ name: 'customerCustomField', type: 'string', list: false },
|
|
533
|
+
]);
|
|
534
|
+
customFieldsConfig.set('Address', [{ name: 'addressCustomField', type: 'string', list: false }]);
|
|
535
|
+
customFieldsConfig.set('Product', [{ name: 'productCustomField', type: 'string', list: false }]);
|
|
536
|
+
|
|
537
|
+
const result = addCustomFields(documentNode, { customFieldsMap: customFieldsConfig });
|
|
538
|
+
const printed = print(result);
|
|
539
|
+
|
|
540
|
+
// Should only add customFields to top-level Order entity
|
|
541
|
+
expect(printed).toContain('customFields {');
|
|
542
|
+
expect(printed).toContain('orderCustomField');
|
|
543
|
+
|
|
544
|
+
// Should NOT add customFields to any nested entities
|
|
545
|
+
expect(printed).not.toMatch(/customer\s*\{[^}]*customFields/s);
|
|
546
|
+
expect(printed).not.toMatch(/addresses\s*\{[^}]*customFields/s);
|
|
547
|
+
expect(printed).not.toMatch(/country\s*\{[^}]*customFields/s);
|
|
548
|
+
expect(printed).not.toMatch(/productVariant\s*\{[^}]*customFields/s);
|
|
549
|
+
expect(printed).not.toMatch(/product\s*\{[^}]*customFields/s);
|
|
550
|
+
});
|
|
551
|
+
});
|
|
242
552
|
});
|