Skip to content

Commit 68d7999

Browse files
mirekmcursoragent
andauthored
Harden product type tab count deduping and add missing tests (#6722)
* Harden product type tab count deduping and add missing tests * Default attribute list group-by-type on and fix test types. New sessions see type tabs on product and model attribute lists by default, with ripple copy updated accordingly. Test helpers use NonNullable aliases for nullable GraphQL query fields. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract messages --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c4729a2 commit 68d7999

10 files changed

Lines changed: 173 additions & 24 deletions

File tree

locale/defaultMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,10 @@
501501
"context": "countries list menu label when no countries are assigned",
502502
"string": "There are no countries assigned"
503503
},
504+
"0a93bd": {
505+
"context": "success message after unassigning attributes from a model type",
506+
"string": "Model type updated"
507+
},
504508
"0dCGBW": {
505509
"context": "button",
506510
"string": "Create structure"
@@ -4009,6 +4013,10 @@
40094013
"context": "onboarding step title",
40104014
"string": "Create a new product"
40114015
},
4016+
"IEkGwG": {
4017+
"context": "success message after unassigning attributes from a product type",
4018+
"string": "Product type updated"
4019+
},
40124020
"IFWHn0": {
40134021
"context": "error message",
40144022
"string": "Billing address is not set"

src/attributes/components/AttributeListPage/AttributeListPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ const AttributeListPage = ({
6969
onSelectAttributesIds,
7070
hidePagination = false,
7171
canGroupByType = false,
72-
groupByType = false,
72+
groupByType = true,
7373
onGroupByTypeChange,
7474
showTypeTabs = false,
7575
types,

src/attributes/components/AttributeListPage/messages.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,14 @@ export const attributeListPageMessages = defineMessages({
2323
"Attributes are removed from this model type only. They are not deleted and remain available for other model types. Models that use other types are not affected.",
2424
description: "helper text in unassign dialog on model attributes list, type tab",
2525
},
26+
productTypeUpdated: {
27+
id: "IEkGwG",
28+
defaultMessage: "Product type updated",
29+
description: "success message after unassigning attributes from a product type",
30+
},
31+
modelTypeUpdated: {
32+
id: "0a93bd",
33+
defaultMessage: "Model type updated",
34+
description: "success message after unassigning attributes from a model type",
35+
},
2636
});

src/attributes/hooks/useAttributeGroupByType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const ATTRIBUTE_LIST_GROUP_BY_TYPE_KEY = "attributeList.groupByType";
55
export const useAttributeGroupByType = () => {
66
const [groupByType, setGroupByType] = useLocalStorage<boolean>(
77
ATTRIBUTE_LIST_GROUP_BY_TYPE_KEY,
8-
false,
8+
true,
99
);
1010

1111
return {

src/attributes/ripples/attributeListGroupByType.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ const settingsIconStyle: CSSProperties = {
99

1010
const attributeListGroupByTypeContextual = (
1111
<Text color="default2">
12-
Turn on <em>Group by type</em> in the view menu{" "}
13-
<Settings size={16} aria-hidden style={settingsIconStyle} /> to browse attributes by product or
14-
model type. Your filters stay above the tabs, and you can unassign attributes from a single type
15-
when one tab is selected.
12+
Product and model attribute lists are grouped by type. Use the tabs to browse attributes per
13+
type, or turn off <em>Group by type</em> in the view menu{" "}
14+
<Settings size={16} aria-hidden style={settingsIconStyle} />. Your filters stay above the tabs,
15+
and you can unassign attributes from a single type when one tab is selected.
1616
</Text>
1717
);
1818

@@ -25,6 +25,6 @@ export const rippleAttributeListGroupByType: Ripple = {
2525
oneLiner: "Group attributes by type",
2626
contextual: attributeListGroupByTypeContextual,
2727
global:
28-
"On product and model attribute lists, open the view menu next to Create attribute and choose Group by type. The list adds tabs for each type so you can see which attributes belong to it. Filters and search stay above the tabs. When a single type tab is active, you can unassign selected attributes from that type without deleting them shop-wide.",
28+
"On product and model attribute lists, attributes are grouped by type with tabs for each type. Filters and search stay above the tabs. When a single type tab is active, you can unassign selected attributes from that type without deleting them shop-wide. Turn off Group by type in the view menu next to Create attribute if you prefer the flat list.",
2929
},
3030
};

src/attributes/utils/computeTypeTabCounts.test.ts

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,55 @@
1-
import { computePageTypeTabCounts } from "@dashboard/attributes/utils/computeTypeTabCounts";
2-
import { type PageTypeListWithAssignedAttributeCountsQuery } from "@dashboard/graphql";
1+
import {
2+
computePageTypeTabCounts,
3+
computeProductTypeTabCounts,
4+
} from "@dashboard/attributes/utils/computeTypeTabCounts";
5+
import {
6+
type PageTypeListWithAssignedAttributeCountsQuery,
7+
ProductTypeKindEnum,
8+
type ProductTypeListWithAssignedAttributeCountsQuery,
9+
} from "@dashboard/graphql";
10+
11+
type ProductTypesConnection = NonNullable<
12+
ProductTypeListWithAssignedAttributeCountsQuery["productTypes"]
13+
>;
14+
type ProductTypeNode = ProductTypesConnection["edges"][number]["node"];
15+
16+
const emptyPageInfo = {
17+
__typename: "PageInfo" as const,
18+
endCursor: null,
19+
hasNextPage: false,
20+
hasPreviousPage: false,
21+
startCursor: null,
22+
};
23+
24+
const createProductTypeTabCountsQuery = (
25+
productType: ProductTypeNode,
26+
): ProductTypeListWithAssignedAttributeCountsQuery => ({
27+
__typename: "Query",
28+
productTypes: {
29+
__typename: "ProductTypeCountableConnection",
30+
edges: [
31+
{
32+
__typename: "ProductTypeCountableEdge",
33+
node: productType,
34+
},
35+
],
36+
pageInfo: emptyPageInfo,
37+
},
38+
});
39+
40+
const createProductTypeNode = (
41+
attributes: Pick<ProductTypeNode, "productAttributes" | "variantAttributes">,
42+
): ProductTypeNode => ({
43+
__typename: "ProductType",
44+
id: "pt-1",
45+
name: "Default",
46+
slug: "default",
47+
kind: ProductTypeKindEnum.NORMAL,
48+
hasVariants: true,
49+
isShippingRequired: true,
50+
taxClass: null,
51+
...attributes,
52+
});
353

454
describe("computePageTypeTabCounts", () => {
555
it("should build counts from page type attribute ids", () => {
@@ -20,13 +70,7 @@ describe("computePageTypeTabCounts", () => {
2070
},
2171
},
2272
],
23-
pageInfo: {
24-
__typename: "PageInfo",
25-
endCursor: null,
26-
hasNextPage: false,
27-
hasPreviousPage: false,
28-
startCursor: null,
29-
},
73+
pageInfo: emptyPageInfo,
3074
},
3175
};
3276

@@ -39,3 +83,50 @@ describe("computePageTypeTabCounts", () => {
3983
});
4084
});
4185
});
86+
87+
describe("computeProductTypeTabCounts", () => {
88+
it("should build counts from product and variant attribute ids", () => {
89+
// Arrange
90+
const data = createProductTypeTabCountsQuery(
91+
createProductTypeNode({
92+
productAttributes: [
93+
{ __typename: "Attribute", id: "a-1" },
94+
{ __typename: "Attribute", id: "a-2" },
95+
],
96+
variantAttributes: [{ __typename: "Attribute", id: "a-3" }],
97+
}),
98+
);
99+
100+
// Act
101+
const result = computeProductTypeTabCounts(data);
102+
103+
// Assert
104+
expect(result).toEqual({
105+
"pt-1": { value: 3, hasMore: false },
106+
});
107+
});
108+
109+
it("should dedupe attribute ids shared between product and variant attributes", () => {
110+
// Arrange
111+
const data = createProductTypeTabCountsQuery(
112+
createProductTypeNode({
113+
productAttributes: [
114+
{ __typename: "Attribute", id: "a-1" },
115+
{ __typename: "Attribute", id: "a-2" },
116+
],
117+
variantAttributes: [
118+
{ __typename: "Attribute", id: "a-2" },
119+
{ __typename: "Attribute", id: "a-3" },
120+
],
121+
}),
122+
);
123+
124+
// Act
125+
const result = computeProductTypeTabCounts(data);
126+
127+
// Assert
128+
expect(result).toEqual({
129+
"pt-1": { value: 3, hasMore: false },
130+
});
131+
});
132+
});

src/attributes/utils/computeTypeTabCounts.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { countUniqueAttributeIds } from "@dashboard/attributes/utils/countUniqueAttributeIds";
12
import {
23
type PageTypeListWithAssignedAttributeCountsQuery,
34
type ProductTypeListWithAssignedAttributeCountsQuery,
@@ -27,12 +28,9 @@ export const computeProductTypeTabCounts = (
2728
const counts: Record<string, ModelTypeTabCount> = {};
2829

2930
data?.productTypes?.edges?.forEach(({ node }) => {
30-
const ids = new Set<string>();
31-
32-
node.productAttributes?.forEach(attribute => ids.add(attribute.id));
33-
node.variantAttributes?.forEach(attribute => ids.add(attribute.id));
34-
35-
counts[node.id] = toTabCount(ids.size);
31+
counts[node.id] = toTabCount(
32+
countUniqueAttributeIds([node.productAttributes, node.variantAttributes]),
33+
);
3634
});
3735

3836
return counts;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { countUniqueAttributeIds } from "@dashboard/attributes/utils/countUniqueAttributeIds";
2+
3+
describe("countUniqueAttributeIds", () => {
4+
it("should count unique ids across attribute lists", () => {
5+
// Arrange
6+
const productAttributes = [{ id: "a-1" }, { id: "a-2" }];
7+
const variantAttributes = [{ id: "a-3" }];
8+
9+
// Act
10+
const result = countUniqueAttributeIds([productAttributes, variantAttributes]);
11+
12+
// Assert
13+
expect(result).toBe(3);
14+
});
15+
16+
it("should dedupe ids shared between attribute lists", () => {
17+
// Arrange
18+
const productAttributes = [{ id: "a-1" }, { id: "a-2" }];
19+
const variantAttributes = [{ id: "a-2" }, { id: "a-3" }];
20+
21+
// Act
22+
const result = countUniqueAttributeIds([productAttributes, variantAttributes]);
23+
24+
// Assert
25+
expect(result).toBe(3);
26+
});
27+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
interface AttributeId {
2+
id: string;
3+
}
4+
5+
export const countUniqueAttributeIds = (
6+
attributeLists: Array<Array<AttributeId> | null | undefined>,
7+
): number => {
8+
const ids = new Set<string>();
9+
10+
attributeLists.forEach(attributeList => {
11+
attributeList?.forEach(attribute => ids.add(attribute.id));
12+
});
13+
14+
return ids.size;
15+
};

src/attributes/views/AttributeList/AttributeList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,8 @@ const AttributeList = ({ params }: AttributeListProps) => {
369369
status: "success",
370370
text: intl.formatMessage(
371371
assignedTypeKind === "product"
372-
? { id: "6j4TUi", defaultMessage: "Product type updated" }
373-
: { id: "GVGaij", defaultMessage: "Model type updated" },
372+
? attributeListPageMessages.productTypeUpdated
373+
: attributeListPageMessages.modelTypeUpdated,
374374
),
375375
});
376376
clearRowSelection();

0 commit comments

Comments
 (0)