Skip to content

Commit 5474def

Browse files
fix: sort images based on role order (#73)
* fix: sort images based on role order * fix: pass imageRoleOrder --------- Co-authored-by: Dylan Depass <[email protected]>
1 parent 9f0aec2 commit 5474def

File tree

7 files changed

+118
-5
lines changed

7 files changed

+118
-5
lines changed

src/content/queries/cs-product.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
*/
1212

1313
import { forceImagesHTTPS } from '../../utils/http.js';
14-
import { gql, parseRating, parseSpecialToDate } from '../../utils/product.js';
14+
import {
15+
gql,
16+
parseRating,
17+
parseSpecialToDate,
18+
sortImagesByRole,
19+
} from '../../utils/product.js';
1520

1621
function extractMinMaxPrice(data) {
1722
let minPrice = data.priceRange?.minimum ?? data.price;
@@ -32,6 +37,11 @@ function extractMinMaxPrice(data) {
3237
*/
3338
export const adapter = (config, productData) => {
3439
const { minPrice, maxPrice } = extractMinMaxPrice(productData);
40+
const images = sortImagesByRole(
41+
forceImagesHTTPS(productData.images)
42+
?? [],
43+
config.imageRoleOrder,
44+
);
3545

3646
/** @type {Product} */
3747
const product = {
@@ -70,7 +80,7 @@ export const adapter = (config, productData) => {
7080
},
7181
};
7282
}),
73-
images: forceImagesHTTPS(productData.images) ?? [],
83+
images,
7484
attributes: productData.attributes ?? [],
7585
attributeMap: Object.fromEntries((productData.attributes ?? [])
7686
.map(({ name, value }) => [name, value])),
@@ -170,6 +180,7 @@ export default ({ sku, imageRoles = [], linkTypes = [] }) => gql`{
170180
images(roles: [${imageRoles.map((s) => `"${s}"`).join(',')}]) {
171181
url
172182
label
183+
roles
173184
}
174185
links(linkTypes: [${linkTypes.map((s) => `"${s}"`).join(',')}]) {
175186
product {

src/content/queries/cs-variants.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
*/
1212

1313
import { forceImagesHTTPS } from '../../utils/http.js';
14-
import { gql, parseRating, parseSpecialToDate } from '../../utils/product.js';
14+
import {
15+
gql,
16+
parseRating,
17+
parseSpecialToDate,
18+
sortImagesByRole,
19+
} from '../../utils/product.js';
1520

1621
/**
1722
* @param {Config} config
@@ -22,14 +27,16 @@ export const adapter = (config, variants) => variants.map(({ selections, product
2227
const minPrice = product.priceRange?.minimum ?? product.price;
2328
const maxPrice = product.priceRange?.maximum ?? product.price;
2429

30+
const images = sortImagesByRole(forceImagesHTTPS(product.images) ?? [], config.imageRoleOrder);
31+
2532
/** @type {Variant} */
2633
const variant = {
2734
name: product.name,
2835
sku: product.sku,
2936
description: product.description,
3037
url: product.url,
3138
inStock: product.inStock,
32-
images: forceImagesHTTPS(product.images) ?? [],
39+
images,
3340
attributes: product.attributes ?? [],
3441
attributeMap: Object.fromEntries((product.attributes ?? [])
3542
.map(({ name, value }) => [name, value])),
@@ -91,6 +98,7 @@ export default ({ sku, imageRoles = [] }) => gql`
9198
images(roles: [${imageRoles.map((s) => `"${s}"`).join(',')}]) {
9299
url
93100
label
101+
roles
94102
}
95103
... on SimpleProductView {
96104
description

src/schemas/Config.js

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ const ConfigEntry = {
4444
offerVariantURLTemplate: { type: 'string' },
4545
liveSearchEnabled: { type: 'boolean' },
4646
attributeOverrides: AttributeOverrides,
47+
imageRoleOrder: {
48+
type: 'array',
49+
items: { type: 'string' },
50+
},
4751
imageParams: {
4852
type: 'object',
4953
properties: {},

src/templates/html/HTMLTemplate.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ ${HTMLTemplate.indent(this.renderProductItems(opt.items), 2)}`).join('\n')}
268268
}
269269

270270
// append image params
271-
const { url: purl, label } = image;
271+
const { url: purl, label, roles = [] } = image;
272272
const params = new URLSearchParams(this.ctx.config.imageParams);
273273
let url;
274274

@@ -283,6 +283,7 @@ ${HTMLTemplate.indent(this.renderProductItems(opt.items), 2)}`).join('\n')}
283283
return {
284284
url: url.toString(),
285285
label,
286+
roles,
286287
};
287288
}
288289

src/types.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ declare global {
6767
*/
6868
imageRoles?: string[];
6969

70+
/**
71+
* Order for images to appear in markup
72+
* If not provided, images will not be sorted
73+
* If image role doesn't exist in the order, it will be appended to the end
74+
*/
75+
imageRoleOrder?: string[];
76+
7077
/**
7178
* Attributes to override using a different attribute name
7279
*/
@@ -140,6 +147,7 @@ declare global {
140147
sku?: string;
141148
matchedPatterns: string[];
142149
imageRoles?: string[];
150+
imageRoleOrder?: string[];
143151
linkTypes?: string[];
144152
host: string;
145153
params: Record<string, string>;
@@ -258,6 +266,7 @@ declare global {
258266
interface Image {
259267
url: string;
260268
label: string;
269+
roles: string[];
261270
}
262271

263272
interface Price {

src/utils/product.js

+22
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,25 @@ export function parseRating(product) {
154154
}
155155
return undefined;
156156
}
157+
158+
/**
159+
* @param {Product['images']} images images with roles
160+
* @param {string[]} [order] preference order
161+
* @returns {Product['images']}
162+
*/
163+
export function sortImagesByRole(images, order) {
164+
if (!order?.length) {
165+
return images;
166+
}
167+
168+
const sorted = [];
169+
let remaining = images;
170+
order.forEach((role) => {
171+
const found = remaining.filter((img) => img.roles.includes(role));
172+
if (found.length) {
173+
sorted.push(...found);
174+
remaining = remaining.filter((img) => !found.includes(img));
175+
}
176+
});
177+
return sorted.concat(remaining);
178+
}

test/utils/product.test.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2025 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import { strict as assert } from 'node:assert';
14+
import { sortImagesByRole } from '../../src/utils/product.js';
15+
16+
describe('product utils', () => {
17+
describe('sortImagesByRole()', () => {
18+
it('should sort images by role', () => {
19+
/** @type {Product['images']} */
20+
const images = [{
21+
url: 'https://example.com/image4.jpg',
22+
label: 'four',
23+
roles: ['teriary'],
24+
}, {
25+
url: 'https://example.com/image1.jpg',
26+
label: 'one',
27+
roles: ['thumbnail'],
28+
}, {
29+
url: 'https://example.com/image2.jpg',
30+
label: 'two',
31+
roles: ['primary'],
32+
}, {
33+
url: 'https://example.com/image3.jpg',
34+
label: 'three',
35+
roles: ['thumbnail', 'secondary'],
36+
}];
37+
38+
const sortedImages = sortImagesByRole(images, ['thumbnail', 'primary']);
39+
assert.deepStrictEqual(sortedImages, [{
40+
url: 'https://example.com/image1.jpg',
41+
label: 'one',
42+
roles: ['thumbnail'],
43+
}, {
44+
url: 'https://example.com/image3.jpg',
45+
label: 'three',
46+
roles: ['thumbnail', 'secondary'],
47+
}, {
48+
url: 'https://example.com/image2.jpg',
49+
label: 'two',
50+
roles: ['primary'],
51+
}, {
52+
url: 'https://example.com/image4.jpg',
53+
label: 'four',
54+
roles: ['teriary'],
55+
}]);
56+
});
57+
});
58+
});

0 commit comments

Comments
 (0)