@akinon/pz-theme 2.0.150-rc.0 → 2.0.151-beta.0
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 +18 -35
- package/package.json +4 -4
- package/src/__tests__/collection-widget-core-path.test.ts +167 -0
- package/src/blocks/favorite-button-block.tsx +4 -3
- package/src/blocks/product-recommendations-block.tsx +2 -2
- package/src/blocks/reset-password-token-check.ts +6 -5
- package/src/load-placeholder-sections.ts +9 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,40 +1,23 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- Updated dependencies [
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
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 [1595d458e]
|
|
35
|
-
- Updated dependencies [dca128112]
|
|
36
|
-
- @akinon/next@2.0.150-rc.0
|
|
37
|
-
- @akinon/theme-contract@2.0.150-rc.0
|
|
3
|
+
## 2.0.151-beta.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [cbbbfd757]
|
|
8
|
+
- @akinon/next@2.0.151-beta.0
|
|
9
|
+
- @akinon/theme-contract@2.0.151-beta.0
|
|
10
|
+
|
|
11
|
+
## 2.0.150
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 6532002: ZERO-5207: Correct what pz-theme's notes say each core provides, and pin the collection widget fallback
|
|
16
|
+
|
|
17
|
+
The notes in the server loader, the favourite button, the recommendations block and the reset password check described every gap as a v1 gap. On the latest cores only one remains: `getCollectionWidgetData` (added to the v1 line by ZERO-5207). No core, 1.x or 2.x, has a recommendation or a per-product favourite endpoint, and the reset token query has been on the 1.x line since 1.121.0. A new test pins both halves of the collection widget path: through the core when it exports `getCollectionWidgetData`, straight to the endpoint when it does not. No runtime change.
|
|
18
|
+
|
|
19
|
+
- @akinon/next@2.0.150
|
|
20
|
+
- @akinon/theme-contract@2.0.150
|
|
38
21
|
|
|
39
22
|
## 2.0.149
|
|
40
23
|
|
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.
|
|
4
|
+
"version": "2.0.151-beta.0",
|
|
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.
|
|
11
|
+
"@akinon/next": "2.0.151-beta.0",
|
|
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.
|
|
16
|
+
"@akinon/theme-contract": "2.0.151-beta.0",
|
|
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.
|
|
24
|
+
"@akinon/next": "2.0.151-beta.0",
|
|
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,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a collection-backed section's rows come from on the server.
|
|
3
|
+
*
|
|
4
|
+
* A section bound to a product collection is saved as a collection widget
|
|
5
|
+
* (`/product_collection_widgets/<slug>/`), so its regular widget answer is
|
|
6
|
+
* empty and the loader asks for the collection widget instead. A core that
|
|
7
|
+
* exports `getCollectionWidgetData` answers through its cache: every 2.x core,
|
|
8
|
+
* and the 1.x line from ZERO-5207 on. An older 1.x core has no such export, so
|
|
9
|
+
* the loader calls the same endpoint itself, uncached.
|
|
10
|
+
*
|
|
11
|
+
* Both paths must hand the renderer the same rows; only the cache differs.
|
|
12
|
+
*/
|
|
13
|
+
jest.mock('server-only', () => ({}), { virtual: true });
|
|
14
|
+
|
|
15
|
+
const getWidgetData = jest.fn();
|
|
16
|
+
const getWidgetSchemaData = jest.fn();
|
|
17
|
+
const getCollectionWidgetData = jest.fn();
|
|
18
|
+
const getPreviewSnapshot = jest.fn();
|
|
19
|
+
const appFetch = jest.fn();
|
|
20
|
+
|
|
21
|
+
/** The core's server data layer, as this test shapes it. */
|
|
22
|
+
const core: Record<string, unknown> = {
|
|
23
|
+
__esModule: true,
|
|
24
|
+
getWidgetData: (...args: unknown[]) => getWidgetData(...args),
|
|
25
|
+
getWidgetSchemaData: (...args: unknown[]) => getWidgetSchemaData(...args),
|
|
26
|
+
getCollectionWidgetData: (...args: unknown[]) =>
|
|
27
|
+
getCollectionWidgetData(...args),
|
|
28
|
+
getPreviewSnapshot: (...args: unknown[]) => getPreviewSnapshot(...args)
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
jest.mock('@akinon/next/data/server', () => core);
|
|
32
|
+
|
|
33
|
+
jest.mock(
|
|
34
|
+
'@akinon/next/utils/app-fetch',
|
|
35
|
+
() => ({
|
|
36
|
+
__esModule: true,
|
|
37
|
+
default: (...args: unknown[]) => appFetch(...args)
|
|
38
|
+
}),
|
|
39
|
+
{ virtual: true }
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
jest.mock(
|
|
43
|
+
'@akinon/next/utils/server-variables',
|
|
44
|
+
() => ({ ServerVariables: { locale: 'tr', currency: 'try' } }),
|
|
45
|
+
{ virtual: true }
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
import { loadPlaceholderSections } from '../load-placeholder-sections';
|
|
49
|
+
|
|
50
|
+
const SLOT = 'homepage';
|
|
51
|
+
const SECTION = 'collection-slider';
|
|
52
|
+
const SOURCE_ID = 'ds-1';
|
|
53
|
+
const COLLECTION_ENDPOINT = `/product_collection_widgets/${SECTION}/`;
|
|
54
|
+
|
|
55
|
+
const LIVE_ROWS = [
|
|
56
|
+
{ pk: 1, name: 'Live one', absolute_url: '/live-one/', price: '199.99' }
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/** The collection widget's answer: the bound collection's live rows. */
|
|
60
|
+
const COLLECTION_WIDGET = {
|
|
61
|
+
slug: SECTION,
|
|
62
|
+
attributes: {},
|
|
63
|
+
products: LIVE_ROWS
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const withCollectionExport = (present: boolean) => {
|
|
67
|
+
core.getCollectionWidgetData = present
|
|
68
|
+
? (...args: unknown[]) => getCollectionWidgetData(...args)
|
|
69
|
+
: undefined;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const loadSection = async () => {
|
|
73
|
+
getWidgetData.mockImplementation(async ({ slug }: { slug: string }) => {
|
|
74
|
+
if (slug === SLOT) {
|
|
75
|
+
return {
|
|
76
|
+
slug: SLOT,
|
|
77
|
+
attributes: {
|
|
78
|
+
theme_editor_placeholder: {
|
|
79
|
+
value: JSON.stringify([{ section_slug: SECTION, order: 0 }])
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (slug === 'theme-config') {
|
|
86
|
+
return {
|
|
87
|
+
slug: 'theme-config',
|
|
88
|
+
attributes: {
|
|
89
|
+
theme_dataSources: {
|
|
90
|
+
value: JSON.stringify([
|
|
91
|
+
{
|
|
92
|
+
id: SOURCE_ID,
|
|
93
|
+
name: 'Picked collection',
|
|
94
|
+
type: 'collection',
|
|
95
|
+
details: { collection: { id: 'c-1', products: [] } }
|
|
96
|
+
}
|
|
97
|
+
])
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The section itself is a collection widget: the regular endpoint has
|
|
104
|
+
// nothing for it.
|
|
105
|
+
return null;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
getWidgetSchemaData.mockResolvedValue({
|
|
109
|
+
schema: {
|
|
110
|
+
[SECTION]: {
|
|
111
|
+
metadata: {
|
|
112
|
+
name: 'Product slider',
|
|
113
|
+
dataSourceId: SOURCE_ID,
|
|
114
|
+
blocks: []
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const loaded = await loadPlaceholderSections(SLOT);
|
|
121
|
+
return loaded?.sections[0];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const collectionEndpointCalls = () =>
|
|
125
|
+
appFetch.mock.calls.filter(([args]) => args?.url === COLLECTION_ENDPOINT);
|
|
126
|
+
|
|
127
|
+
beforeEach(() => {
|
|
128
|
+
getWidgetData.mockReset();
|
|
129
|
+
getWidgetSchemaData.mockReset();
|
|
130
|
+
getCollectionWidgetData.mockReset().mockResolvedValue(COLLECTION_WIDGET);
|
|
131
|
+
getPreviewSnapshot.mockReset().mockResolvedValue(null);
|
|
132
|
+
appFetch.mockReset().mockResolvedValue(COLLECTION_WIDGET);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
afterAll(() => {
|
|
136
|
+
withCollectionExport(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('a core that exports getCollectionWidgetData', () => {
|
|
140
|
+
beforeEach(() => withCollectionExport(true));
|
|
141
|
+
|
|
142
|
+
it("reads the section's collection widget through the core", async () => {
|
|
143
|
+
const section = await loadSection();
|
|
144
|
+
|
|
145
|
+
expect(getCollectionWidgetData).toHaveBeenCalledWith({ slug: SECTION });
|
|
146
|
+
expect(collectionEndpointCalls()).toHaveLength(0);
|
|
147
|
+
expect(section?.dataSource.details.collection.data).toEqual(
|
|
148
|
+
COLLECTION_WIDGET
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('an older 1.x core without it', () => {
|
|
154
|
+
beforeEach(() => withCollectionExport(false));
|
|
155
|
+
|
|
156
|
+
it('calls the collection widget endpoint itself and renders the same rows', async () => {
|
|
157
|
+
const section = await loadSection();
|
|
158
|
+
|
|
159
|
+
expect(getCollectionWidgetData).not.toHaveBeenCalled();
|
|
160
|
+
expect(collectionEndpointCalls()).toEqual([
|
|
161
|
+
[{ url: COLLECTION_ENDPOINT, locale: 'tr', currency: 'try' }]
|
|
162
|
+
]);
|
|
163
|
+
expect(section?.dataSource.details.collection.data).toEqual(
|
|
164
|
+
COLLECTION_WIDGET
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -111,9 +111,10 @@ const FavoriteButtonBlock = ({
|
|
|
111
111
|
const productPk = Number(pickProductField(product, 'pk'));
|
|
112
112
|
const hasProduct = Number.isFinite(productPk) && productPk > 0;
|
|
113
113
|
|
|
114
|
-
//
|
|
115
|
-
// state is derived from the favourites list. It is skipped for
|
|
116
|
-
// endpoint 401s) and in the designer (no shopper session to
|
|
114
|
+
// No core, 1.x or 2.x, has an "is this product favourited" endpoint, so the
|
|
115
|
+
// current state is derived from the favourites list. It is skipped for
|
|
116
|
+
// guests (the endpoint 401s) and in the designer (no shopper session to
|
|
117
|
+
// reflect).
|
|
117
118
|
const { data: favorites } = useGetFavoritesQuery(
|
|
118
119
|
{ limit: FAVORITES_PAGE_SIZE },
|
|
119
120
|
{ skip: !isAuthenticated || !hasProduct || isDesigner }
|
|
@@ -219,8 +219,8 @@ interface RecommendationCard {
|
|
|
219
219
|
/**
|
|
220
220
|
* Functional PDP/campaign block: a related-products slider.
|
|
221
221
|
*
|
|
222
|
-
*
|
|
223
|
-
* collection/widget one every storefront already uses: the author names a
|
|
222
|
+
* No core, 1.x or 2.x, ships a recommendation endpoint, so the data path is
|
|
223
|
+
* the collection/widget one every storefront already uses: the author names a
|
|
224
224
|
* collection and `getWidget` returns its products in merchandiser order. The
|
|
225
225
|
* cards are laid out exactly like the product grid so a recommendation strip
|
|
226
226
|
* and a PLP card stay visually identical.
|
|
@@ -45,11 +45,12 @@ const useUncheckedResetToken: ValidateResetTokenHook = () => UNCHECKED;
|
|
|
45
45
|
/**
|
|
46
46
|
* Read off the module object instead of imported by name.
|
|
47
47
|
*
|
|
48
|
-
* `useGetValidatePasswordResetTokenQuery
|
|
49
|
-
*
|
|
50
|
-
* this package
|
|
51
|
-
*
|
|
52
|
-
*
|
|
48
|
+
* Every 2.x core has `useGetValidatePasswordResetTokenQuery`, but it reached
|
|
49
|
+
* the 1.x line only in 1.121.0 (ZERO-4009), and brands on an older 1.x core
|
|
50
|
+
* still run this package. A brand transpiles and type-checks this package
|
|
51
|
+
* from `src`, so a named import of an export its core does not have fails
|
|
52
|
+
* `next build` before the block can ever run — while a bare runtime lookup
|
|
53
|
+
* would leave `undefined` in a hook position and crash the screen.
|
|
53
54
|
*
|
|
54
55
|
* Both are answered here: the name never appears in an import clause, so the
|
|
55
56
|
* v1 type-checker has nothing to miss, and the fallback above is bound in its
|
|
@@ -21,14 +21,16 @@ import { carriesProducts } from './utils/live-rows';
|
|
|
21
21
|
const { getWidgetData } = serverData;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
* `getWidgetSchemaData` and `getCollectionWidgetData
|
|
24
|
+
* Older 1.x cores ship a smaller server data layer: every 2.x core has
|
|
25
|
+
* `getWidgetSchemaData` and `getCollectionWidgetData`, but they reached the
|
|
26
|
+
* 1.x line only with ZERO-5194 and ZERO-5207.
|
|
26
27
|
*
|
|
27
|
-
* The endpoints are identical on both backends —
|
|
28
|
-
* Degrading to "no data" would render every published section
|
|
29
|
-
* blocks* (a section's block tree lives in the widget schema, not
|
|
30
|
-
* widget), so on
|
|
31
|
-
* composed pages rendering on the
|
|
28
|
+
* The endpoints are identical on both backends — those cores just never
|
|
29
|
+
* wrapped them. Degrading to "no data" would render every published section
|
|
30
|
+
* *without its blocks* (a section's block tree lives in the widget schema, not
|
|
31
|
+
* in the widget), so on such a core we call the REST endpoints directly,
|
|
32
|
+
* without the core's cache. This is what keeps composed pages rendering on the
|
|
33
|
+
* brands whose core predates them.
|
|
32
34
|
*/
|
|
33
35
|
type ServerDataFn = (<T>(args: never) => Promise<T | null>) | undefined;
|
|
34
36
|
|