@akinon/pz-theme 2.0.108-rc.0 → 2.0.108
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/CHANGELOG.md +7 -35
- package/package.json +4 -4
- package/src/__tests__/section-live-rows.test.ts +248 -0
- package/src/load-placeholder-sections.ts +78 -35
package/CHANGELOG.md
CHANGED
|
@@ -1,40 +1,12 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.108
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- Updated dependencies [51ea06888]
|
|
11
|
-
- Updated dependencies
|
|
12
|
-
- Updated dependencies [2fea41430]
|
|
13
|
-
- Updated dependencies [b55acb768]
|
|
14
|
-
- Updated dependencies [760258c1c]
|
|
15
|
-
- Updated dependencies [143be2b9d]
|
|
16
|
-
- Updated dependencies [7889b08fe]
|
|
17
|
-
- Updated dependencies [9f8cd3bc5]
|
|
18
|
-
- Updated dependencies [bfafa3f49]
|
|
19
|
-
- Updated dependencies [57d7eb305]
|
|
20
|
-
- Updated dependencies [d99a6a7d5]
|
|
21
|
-
- Updated dependencies [9db81a714]
|
|
22
|
-
- Updated dependencies [591e345e1]
|
|
23
|
-
- Updated dependencies [4de5303c5]
|
|
24
|
-
- Updated dependencies [95b139dc1]
|
|
25
|
-
- Updated dependencies [1d00f2d06]
|
|
26
|
-
- Updated dependencies [4ac7b2a1e]
|
|
27
|
-
- Updated dependencies [e9598c71d]
|
|
28
|
-
- Updated dependencies [4998a9631]
|
|
29
|
-
- Updated dependencies [804d2bd6c]
|
|
30
|
-
- Updated dependencies [3909d3224]
|
|
31
|
-
- Updated dependencies [6a3d8a631]
|
|
32
|
-
- Updated dependencies [e18836b20]
|
|
33
|
-
- Updated dependencies [b1111d7b1]
|
|
34
|
-
- Updated dependencies [1595d45]
|
|
35
|
-
- Updated dependencies [dca1281]
|
|
36
|
-
- @akinon/next@2.0.108-rc.0
|
|
37
|
-
- @akinon/theme-contract@2.0.108-rc.0
|
|
3
|
+
## 2.0.108
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6ec2ef3: ZERO-5080: Render collection-backed section products server-side instead of the editor's frozen snapshot
|
|
8
|
+
- @akinon/next@2.0.108
|
|
9
|
+
- @akinon/theme-contract@2.0.108
|
|
38
10
|
|
|
39
11
|
## 2.0.107
|
|
40
12
|
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.108
|
|
4
|
+
"version": "2.0.108",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.108
|
|
11
|
+
"@akinon/next": "2.0.108",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.108
|
|
16
|
+
"@akinon/theme-contract": "2.0.108",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"react-multi-carousel": "2.8.4",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"zod": "4.4.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@akinon/next": "2.0.108
|
|
24
|
+
"@akinon/next": "2.0.108",
|
|
25
25
|
"@testing-library/jest-dom": "5.16.4",
|
|
26
26
|
"@testing-library/react": "16.3.2",
|
|
27
27
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A section bound to a collection has to render its products on the SERVER.
|
|
3
|
+
*
|
|
4
|
+
* It did not: `details.collection.data` was only written when the data source
|
|
5
|
+
* carried an editor-persisted `collection.slug`, and the editor never persists
|
|
6
|
+
* one -- so every brand fell through to `details.collection.products`, the
|
|
7
|
+
* snapshot frozen when the collection was picked. On a force-static page that
|
|
8
|
+
* snapshot is what every shopper, crawler and no-JS visit gets: rows with no
|
|
9
|
+
* `absolute_url` (every card linking to `#`) at whatever price the catalogue
|
|
10
|
+
* showed that day, swapped out visibly once the client refetched.
|
|
11
|
+
*
|
|
12
|
+
* The live rows were already in hand the whole time: the loader fetches each
|
|
13
|
+
* section's own widget for its block values, and for a collection-backed
|
|
14
|
+
* section that answer carries the bound collection's live products.
|
|
15
|
+
*/
|
|
16
|
+
jest.mock('server-only', () => ({}), { virtual: true });
|
|
17
|
+
|
|
18
|
+
const getWidgetData = jest.fn();
|
|
19
|
+
const getWidgetSchemaData = jest.fn();
|
|
20
|
+
const getCollectionWidgetData = jest.fn();
|
|
21
|
+
const getPreviewSnapshot = jest.fn(async () => null as unknown);
|
|
22
|
+
|
|
23
|
+
jest.mock('@akinon/next/data/server', () => ({
|
|
24
|
+
getWidgetData: (...args: unknown[]) => getWidgetData(...args),
|
|
25
|
+
getWidgetSchemaData: (...args: unknown[]) => getWidgetSchemaData(...args),
|
|
26
|
+
getCollectionWidgetData: (...args: unknown[]) =>
|
|
27
|
+
getCollectionWidgetData(...args),
|
|
28
|
+
getPreviewSnapshot: (...args: unknown[]) =>
|
|
29
|
+
(getPreviewSnapshot as (...a: unknown[]) => unknown)(...args)
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
jest.mock(
|
|
33
|
+
'@akinon/next/utils/app-fetch',
|
|
34
|
+
() => ({ __esModule: true, default: jest.fn() }),
|
|
35
|
+
{ virtual: true }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
jest.mock(
|
|
39
|
+
'@akinon/next/utils/server-variables',
|
|
40
|
+
() => ({ ServerVariables: { locale: 'tr', currency: 'try' } }),
|
|
41
|
+
{ virtual: true }
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
import { loadPlaceholderSections } from '../load-placeholder-sections';
|
|
45
|
+
|
|
46
|
+
const SLOT = 'homepage';
|
|
47
|
+
const SECTION = 'rmd8i';
|
|
48
|
+
const SOURCE_ID = 'ds-1';
|
|
49
|
+
|
|
50
|
+
/** What the backend returns for a section bound to a live collection. */
|
|
51
|
+
const LIVE_ROWS = [
|
|
52
|
+
{ pk: 1, name: 'Live one', absolute_url: '/live-one/', price: '199.99' },
|
|
53
|
+
{ pk: 2, name: 'Live two', absolute_url: '/live-two/', price: '299.99' }
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** What the editor saved when the designer picked the collection: no links. */
|
|
57
|
+
const SNAPSHOT_ROWS = [
|
|
58
|
+
{ pk: 9, name: 'Frozen one', price: '4999.99' },
|
|
59
|
+
{ pk: 8, name: 'Frozen two', price: '4999.99' }
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const placeholderWidget = () => ({
|
|
63
|
+
slug: SLOT,
|
|
64
|
+
attributes: {
|
|
65
|
+
theme_editor_placeholder: {
|
|
66
|
+
value: JSON.stringify([{ section_slug: SECTION, order: 0 }])
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const themeConfigWidget = (dataSources: unknown[]) => ({
|
|
72
|
+
slug: 'theme-config',
|
|
73
|
+
attributes: { theme_dataSources: { value: JSON.stringify(dataSources) } }
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const collectionSource = (collection: Record<string, unknown>) => ({
|
|
77
|
+
id: SOURCE_ID,
|
|
78
|
+
name: 'Picked collection',
|
|
79
|
+
type: 'collection',
|
|
80
|
+
details: { collection }
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Drives the live path: the placeholder lists one section, the section widget
|
|
85
|
+
* answers with `sectionWidget`, and theme-config carries `dataSources`.
|
|
86
|
+
*/
|
|
87
|
+
const load = async (options: {
|
|
88
|
+
dataSources: unknown[];
|
|
89
|
+
sectionWidget: unknown;
|
|
90
|
+
/** Answer for the collection's own widget slug, when the source has one. */
|
|
91
|
+
collectionWidget?: unknown;
|
|
92
|
+
dataSourceId?: string | null;
|
|
93
|
+
}) => {
|
|
94
|
+
const { dataSources, sectionWidget, collectionWidget = null } = options;
|
|
95
|
+
const dataSourceId =
|
|
96
|
+
options.dataSourceId === undefined ? SOURCE_ID : options.dataSourceId;
|
|
97
|
+
|
|
98
|
+
getWidgetData.mockImplementation(async ({ slug }: { slug: string }) => {
|
|
99
|
+
if (slug === SLOT) return placeholderWidget();
|
|
100
|
+
if (slug === 'theme-config') return themeConfigWidget(dataSources);
|
|
101
|
+
if (slug === SECTION) return sectionWidget;
|
|
102
|
+
return collectionWidget;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
getWidgetSchemaData.mockResolvedValue({
|
|
106
|
+
schema: {
|
|
107
|
+
[SECTION]: {
|
|
108
|
+
metadata: {
|
|
109
|
+
name: 'Product slider',
|
|
110
|
+
...(dataSourceId ? { dataSourceId } : {}),
|
|
111
|
+
blocks: []
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const loaded = await loadPlaceholderSections(SLOT);
|
|
118
|
+
return loaded?.sections[0];
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
getWidgetData.mockReset();
|
|
123
|
+
getWidgetSchemaData.mockReset().mockResolvedValue(null);
|
|
124
|
+
getCollectionWidgetData.mockReset().mockResolvedValue(null);
|
|
125
|
+
getPreviewSnapshot.mockReset().mockResolvedValue(null);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('collection-backed sections on the server', () => {
|
|
129
|
+
it('renders the live rows the section widget already carries', async () => {
|
|
130
|
+
const section = await load({
|
|
131
|
+
dataSources: [collectionSource({ id: 'c-1', products: SNAPSHOT_ROWS })],
|
|
132
|
+
sectionWidget: { slug: SECTION, attributes: {}, products: LIVE_ROWS }
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// `data` is what the storefront reads first, and it holds the whole widget
|
|
136
|
+
// answer -- the shape the collection-slug path has always written.
|
|
137
|
+
expect(section?.dataSource.details.collection.data).toEqual({
|
|
138
|
+
slug: SECTION,
|
|
139
|
+
attributes: {},
|
|
140
|
+
products: LIVE_ROWS
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('leaves the editor snapshot in place beside the live rows', async () => {
|
|
145
|
+
const section = await load({
|
|
146
|
+
dataSources: [collectionSource({ id: 'c-1', products: SNAPSHOT_ROWS })],
|
|
147
|
+
sectionWidget: { products: LIVE_ROWS }
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// The designer reads `products` first, so the editor keeps showing the
|
|
151
|
+
// collection exactly as it was picked.
|
|
152
|
+
expect(section?.dataSource.details.collection.products).toEqual(
|
|
153
|
+
SNAPSHOT_ROWS
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('does not overwrite a snapshot when the answer carries no products', async () => {
|
|
158
|
+
const section = await load({
|
|
159
|
+
dataSources: [collectionSource({ id: 'c-1', products: SNAPSHOT_ROWS })],
|
|
160
|
+
sectionWidget: { slug: SECTION, attributes: { title: 'Picks' } }
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(section?.dataSource.details.collection.data).toBeUndefined();
|
|
164
|
+
expect(section?.dataSource.details.collection.products).toEqual(
|
|
165
|
+
SNAPSHOT_ROWS
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('does not overwrite a snapshot with an empty product list', async () => {
|
|
170
|
+
const section = await load({
|
|
171
|
+
dataSources: [collectionSource({ id: 'c-1', products: SNAPSHOT_ROWS })],
|
|
172
|
+
sectionWidget: { slug: SECTION, products: [] }
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(section?.dataSource.details.collection.data).toBeUndefined();
|
|
176
|
+
expect(section?.dataSource.details.collection.products).toEqual(
|
|
177
|
+
SNAPSHOT_ROWS
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('keeps the collection-slug refetch in charge when it fires', async () => {
|
|
182
|
+
const refetched = { products: [{ pk: 3, absolute_url: '/from-slug/' }] };
|
|
183
|
+
|
|
184
|
+
const section = await load({
|
|
185
|
+
dataSources: [
|
|
186
|
+
collectionSource({
|
|
187
|
+
id: 'c-1',
|
|
188
|
+
slug: 'collection-widget',
|
|
189
|
+
products: SNAPSHOT_ROWS
|
|
190
|
+
})
|
|
191
|
+
],
|
|
192
|
+
sectionWidget: { products: LIVE_ROWS },
|
|
193
|
+
collectionWidget: refetched
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(section?.dataSource.details.collection.data).toEqual(refetched);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('falls back to the section answer when the slug refetch comes back empty', async () => {
|
|
200
|
+
const section = await load({
|
|
201
|
+
dataSources: [
|
|
202
|
+
collectionSource({ id: 'c-1', slug: 'gone', products: SNAPSHOT_ROWS })
|
|
203
|
+
],
|
|
204
|
+
sectionWidget: { products: LIVE_ROWS },
|
|
205
|
+
collectionWidget: null
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(section?.dataSource.details.collection.data).toEqual({
|
|
209
|
+
products: LIVE_ROWS
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('keeps an already-resolved collection rather than replacing it', async () => {
|
|
214
|
+
const resolved = { products: [{ pk: 4, absolute_url: '/already/' }] };
|
|
215
|
+
|
|
216
|
+
const section = await load({
|
|
217
|
+
dataSources: [
|
|
218
|
+
collectionSource({ id: 'c-1', data: resolved, products: SNAPSHOT_ROWS })
|
|
219
|
+
],
|
|
220
|
+
sectionWidget: { products: LIVE_ROWS }
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(section?.dataSource.details.collection.data).toEqual(resolved);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('leaves a section with no data source untouched', async () => {
|
|
227
|
+
const section = await load({
|
|
228
|
+
dataSources: [],
|
|
229
|
+
sectionWidget: { slug: SECTION, attributes: {}, products: LIVE_ROWS },
|
|
230
|
+
dataSourceId: null
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(section?.dataSourceId).toBeUndefined();
|
|
234
|
+
expect(section?.dataSource).toBeUndefined();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('leaves a non-collection data source untouched', async () => {
|
|
238
|
+
const section = await load({
|
|
239
|
+
dataSources: [
|
|
240
|
+
{ id: SOURCE_ID, type: 'static', details: { static: { data: [] } } }
|
|
241
|
+
],
|
|
242
|
+
sectionWidget: { products: LIVE_ROWS }
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(section?.dataSource.details.collection).toBeUndefined();
|
|
246
|
+
expect(section?.dataSource.details.static).toEqual({ data: [] });
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -62,15 +62,44 @@ const getWidgetSchemaData = async <T,>(args: {
|
|
|
62
62
|
return directFetch<T>(`/widgets/${args.widgetSlug}/schema/`);
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
/** Writes a fetched answer into the field the renderers read as "live rows". */
|
|
66
|
+
const withCollectionData = (dataSource: any, collectionData: unknown): any => ({
|
|
67
|
+
...dataSource,
|
|
68
|
+
details: {
|
|
69
|
+
...dataSource.details,
|
|
70
|
+
collection: {
|
|
71
|
+
...dataSource.details.collection,
|
|
72
|
+
data: collectionData
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A widget answer counts as live rows only when it actually carries a
|
|
79
|
+
* non-empty `products` array: a section with no data source attached answers
|
|
80
|
+
* without the key at all, and an empty list must never replace the editor's
|
|
81
|
+
* snapshot with nothing to render.
|
|
82
|
+
*/
|
|
83
|
+
const carriesProducts = (widgetData: unknown): boolean =>
|
|
84
|
+
!!widgetData &&
|
|
85
|
+
typeof widgetData === 'object' &&
|
|
86
|
+
Array.isArray((widgetData as { products?: unknown }).products) &&
|
|
87
|
+
(widgetData as { products: unknown[] }).products.length > 0;
|
|
88
|
+
|
|
65
89
|
/**
|
|
66
90
|
* Resolves a section's data source reference into the object the renderer
|
|
67
91
|
* expects, fetching collection data for collection-backed sources. Shared by
|
|
68
92
|
* the live render path and the preview-snapshot path so the two cannot drift.
|
|
93
|
+
*
|
|
94
|
+
* `sectionWidgetData` is the section's OWN widget answer, which the live path
|
|
95
|
+
* has already fetched for its block values. See the fallback below for why it
|
|
96
|
+
* is handed over rather than fetched again.
|
|
69
97
|
*/
|
|
70
98
|
async function resolveSectionDataSource(
|
|
71
99
|
dataSourceId: string | undefined,
|
|
72
100
|
dataSources: ThemeConfig['dataSources'],
|
|
73
|
-
sectionSlug: string
|
|
101
|
+
sectionSlug: string,
|
|
102
|
+
sectionWidgetData?: unknown
|
|
74
103
|
): Promise<any | undefined> {
|
|
75
104
|
if (!dataSourceId) {
|
|
76
105
|
return undefined;
|
|
@@ -90,47 +119,60 @@ async function resolveSectionDataSource(
|
|
|
90
119
|
return dataSource;
|
|
91
120
|
}
|
|
92
121
|
|
|
93
|
-
//
|
|
94
|
-
//
|
|
122
|
+
// The editor may persist the collection's own widget slug alongside the
|
|
123
|
+
// source. When it does, that widget is the authority and is refetched here.
|
|
95
124
|
const collectionSlug = dataSource.details.collection?.slug;
|
|
96
|
-
if (!collectionSlug) {
|
|
97
|
-
// The saved products from the editor (details.collection.products)
|
|
98
|
-
// will be used as fallback
|
|
99
|
-
return dataSource;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
let collectionData = await getWidgetData<{
|
|
104
|
-
[key: string]: unknown;
|
|
105
|
-
}>({
|
|
106
|
-
slug: collectionSlug
|
|
107
|
-
});
|
|
108
125
|
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
if (collectionSlug) {
|
|
127
|
+
try {
|
|
128
|
+
let collectionData = await getWidgetData<{
|
|
111
129
|
[key: string]: unknown;
|
|
112
130
|
}>({
|
|
113
131
|
slug: collectionSlug
|
|
114
132
|
});
|
|
115
|
-
}
|
|
116
133
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
134
|
+
if (!collectionData) {
|
|
135
|
+
collectionData = await getCollectionWidgetData<{
|
|
136
|
+
[key: string]: unknown;
|
|
137
|
+
}>({
|
|
138
|
+
slug: collectionSlug
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (collectionData) {
|
|
143
|
+
return withCollectionData(dataSource, collectionData);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(
|
|
147
|
+
`Error fetching widget data for section ${sectionSlug}:`,
|
|
148
|
+
error
|
|
149
|
+
);
|
|
128
150
|
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* No slug, or the refetch came back empty. The section's own widget answer
|
|
155
|
+
* is the second authority: for a collection-backed section the backend
|
|
156
|
+
* returns the bound collection's LIVE rows on it, `absolute_url` and current
|
|
157
|
+
* prices included — and the live path has that answer in hand already.
|
|
158
|
+
*
|
|
159
|
+
* It used to be thrown away, so the server rendered the editor's saved
|
|
160
|
+
* `products` instead: the snapshot frozen when the designer picked the
|
|
161
|
+
* collection. Dead `href="#"` links and yesterday's prices in the served
|
|
162
|
+
* HTML, then a visible swap once the client refetched. Sources saved before
|
|
163
|
+
* the editor began persisting `collection.slug` carry none, so this is the
|
|
164
|
+
* path they take.
|
|
165
|
+
*
|
|
166
|
+
* Only ever fills an ABSENT `data` — a source that already resolved keeps
|
|
167
|
+
* what it resolved to, and the editor reads `products` first either way, so
|
|
168
|
+
* the designer still sees the collection exactly as it was picked.
|
|
169
|
+
*/
|
|
170
|
+
if (
|
|
171
|
+
dataSource.details.collection &&
|
|
172
|
+
!dataSource.details.collection.data &&
|
|
173
|
+
carriesProducts(sectionWidgetData)
|
|
174
|
+
) {
|
|
175
|
+
return withCollectionData(dataSource, sectionWidgetData);
|
|
134
176
|
}
|
|
135
177
|
|
|
136
178
|
return dataSource;
|
|
@@ -366,7 +408,8 @@ export async function loadPlaceholderSections(
|
|
|
366
408
|
const dataSourceWithData = await resolveSectionDataSource(
|
|
367
409
|
dataSourceId,
|
|
368
410
|
dataSources,
|
|
369
|
-
sectionSlug
|
|
411
|
+
sectionSlug,
|
|
412
|
+
sectionData
|
|
370
413
|
);
|
|
371
414
|
|
|
372
415
|
const section: Section = {
|