Skip to content

Commit 255a40f

Browse files
authored
Update lists filtering component (#6879)
* Group variant-reference filter choices under their product A flat variant page hid the size behind a long product name and listed the same product once per variant. * Turn list filters into a foldable panel and quiet the product grid * Let color filters mix with references and give list search the same border as Filters Saleor rejects AttributeInput lists that mix deprecated values with value, so product attributes now send value only. List search uses the existing bordered field, and filter drafts clone so Close no longer shares mutated state with Apply. * Fix date-between filter layout and polish list search chrome * Add changeset * Extract messages * Keep filter-row option CSS from restyling product attributes Global select-option flex rules stretched color swatches, and the variant assign payload overwrote Product: Variant with the bare variant name.
1 parent bc08ccf commit 255a40f

118 files changed

Lines changed: 5496 additions & 678 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"saleor-dashboard": patch
3+
---
4+
5+
General update of the main lists filtering component.
6+
7+
Filters on product, customer, and other lists now open as a foldable panel. Search uses the same bordered field, with syntax hints where Saleor supports them.
8+
9+
Product and variant reference filters show chips with thumbnails; variant choices are grouped under their product. Color (swatch) attributes use a color preview instead of a plain list.
10+
11+
Also fixes applying color together with reference filters, and filter drafts that did not reset cleanly.

.knip.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
"@sentry/cli",
5959
"@react-editor-js/core",
6060
"playwright-ctrf-json-reporter",
61-
"downshift",
6261
"@graphql-codegen/cli",
6362
"@graphql-codegen/fragment-matcher",
6463
"@graphql-codegen/import-types-preset",

locale/defaultMessages.json

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2881,10 +2881,6 @@
28812881
"context": "create product dialog description",
28822882
"string": "Add a product. You can set a price, inventory, and availability after creating it."
28832883
},
2884-
"9B2mOB": {
2885-
"context": "tile view pagination label",
2886-
"string": "No. of products"
2887-
},
28882884
"9C7PZE": {
28892885
"context": "navigation section name",
28902886
"string": "Navigation"
@@ -3033,6 +3029,10 @@
30333029
"context": "collection",
30343030
"string": "Hidden"
30353031
},
3032+
"9exKzi": {
3033+
"context": "Close the list filter panel without clearing applied filters",
3034+
"string": "Close"
3035+
},
30363036
"9gb9b4": {
30373037
"context": "address type",
30383038
"string": "Add new address"
@@ -4492,6 +4492,10 @@
44924492
"context": "dialog content",
44934493
"string": "Select method you want to use to change address"
44944494
},
4495+
"FK088X": {
4496+
"context": "Apply filters from the list filter panel",
4497+
"string": "Apply"
4498+
},
44954499
"FKBNcP": {
44964500
"context": "customer type chip, opens the customer type settings page",
44974501
"string": "View {customerTypeName} type"
@@ -6512,6 +6516,10 @@
65126516
"context": "new discount label",
65136517
"string": "New discount value"
65146518
},
6519+
"MU0Zmt": {
6520+
"context": "product list pagination label",
6521+
"string": "No. of products"
6522+
},
65156523
"MUjTkk": {
65166524
"string": "Search customer types..."
65176525
},
@@ -7816,10 +7824,6 @@
78167824
"context": "copy code button label",
78177825
"string": "Copy code"
78187826
},
7819-
"RZDxej": {
7820-
"context": "Popover trigger text (button)",
7821-
"string": "Filters {count, plural, =0 {} other {({count})} }"
7822-
},
78237827
"RZZAjR": {
78247828
"context": "setup review row title",
78257829
"string": "Taxes"
@@ -16936,6 +16940,10 @@
1693616940
"context": "dialog description when restock warehouse must be selected",
1693716941
"string": "Canceling a fulfillment will restock products at the selected warehouse. The warehouse this fulfillment shipped from is pre-selected."
1693816942
},
16943+
"xzqW+K": {
16944+
"context": "Add condition button on the list filter panel",
16945+
"string": "Add condition"
16946+
},
1693916947
"xzzNba": {
1694016948
"context": "helper under currency on create",
1694116949
"string": "Prices and payments use this currency. It can’t be changed later."

src/attributes/components/SwatchPreview/SwatchPreview.module.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.swatchPreview {
22
display: block;
3+
flex: none;
34
flex-shrink: 0;
45
box-sizing: border-box;
56
border: 1px solid var(--mu-colors-border-default1);

src/attributes/utils/data.test.ts

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,42 @@ describe("attributes/utils/data", () => {
462462

463463
// Assert
464464
expect(result.data.references).toEqual([
465-
{ value: "test-variant-1", label: "Product 1 Variant A" },
465+
{ value: "test-variant-1", label: "Product 1: Variant A" },
466+
]);
467+
});
468+
469+
it("ignores additionalData that is only the raw id and uses search instead", () => {
470+
// Arrange
471+
const variantId = "UHJvZHVjdFZhcmlhbnQ6Mzg0";
472+
const attribute = {
473+
id: "attr-gid-test",
474+
value: [variantId],
475+
label: "Test",
476+
data: {
477+
inputType: AttributeInputTypeEnum.REFERENCE,
478+
entityType: AttributeEntityTypeEnum.PRODUCT_VARIANT,
479+
isRequired: false,
480+
values: [],
481+
references: [],
482+
},
483+
additionalData: [{ value: variantId, label: variantId }],
484+
};
485+
const references = createMockReferenceData({
486+
products: [
487+
{
488+
id: "test-product-1",
489+
name: "White Plimsolls",
490+
variants: [{ id: variantId, name: "44 / White" }],
491+
},
492+
],
493+
});
494+
495+
// Act
496+
const result = getReferenceAttributeDisplayData(attribute, references);
497+
498+
// Assert
499+
expect(result.data.references).toEqual([
500+
{ value: variantId, label: "White Plimsolls: 44 / White" },
466501
]);
467502
});
468503

@@ -534,7 +569,7 @@ describe("attributes/utils/data", () => {
534569

535570
// Assert
536571
expect(result.data.references).toEqual([
537-
{ value: "v3", label: "Product 3 Variant 3" },
572+
{ value: "v3", label: "Product 3: Variant 3" },
538573
{ value: "non-existent", label: "non-existent" }, // Fallback
539574
]);
540575
});
@@ -646,14 +681,14 @@ describe("attributes/utils/data", () => {
646681

647682
// Assert - Verify cache is working
648683
expect(result1.data.references).toEqual([
649-
{ value: "cache-variant-1", label: "Cache Test Product Cache Variant 1" },
684+
{ value: "cache-variant-1", label: "Cache Test Product: Cache Variant 1" },
650685
]);
651686
expect(result2.data.references).toEqual([
652-
{ value: "cache-variant-2", label: "Cache Test Product Cache Variant 2" },
687+
{ value: "cache-variant-2", label: "Cache Test Product: Cache Variant 2" },
653688
]);
654689
expect(result3.data.references).toEqual([
655-
{ value: "cache-variant-3", label: "Cache Test Product Cache Variant 3" },
656-
{ value: "cache-variant-1", label: "Cache Test Product Cache Variant 1" },
690+
{ value: "cache-variant-3", label: "Cache Test Product: Cache Variant 3" },
691+
{ value: "cache-variant-1", label: "Cache Test Product: Cache Variant 1" },
657692
]);
658693

659694
// productVariants is read once while building the cache; later lookups reuse it
@@ -711,10 +746,10 @@ describe("attributes/utils/data", () => {
711746

712747
// Assert
713748
expect(result.data.references).toEqual([
714-
{ value: "sep-p1-v1", label: "Separate Cache Product 1 P1 Variant 1" },
715-
{ value: "sep-p2-v1", label: "Separate Cache Product 2 P2 Variant 1" },
716-
{ value: "sep-p1-v2", label: "Separate Cache Product 1 P1 Variant 2" },
717-
{ value: "sep-p2-v2", label: "Separate Cache Product 2 P2 Variant 2" },
749+
{ value: "sep-p1-v1", label: "Separate Cache Product 1: P1 Variant 1" },
750+
{ value: "sep-p2-v1", label: "Separate Cache Product 2: P2 Variant 1" },
751+
{ value: "sep-p1-v2", label: "Separate Cache Product 1: P1 Variant 2" },
752+
{ value: "sep-p2-v2", label: "Separate Cache Product 2: P2 Variant 2" },
718753
]);
719754

720755
expect(product1GetterCalls).toBe(1);

src/attributes/utils/data.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
} from "@dashboard/utils/richText/useMultipleRichText";
3030

3131
import { type AttributePageFormData } from "../components/AttributePage";
32+
import { formatVariantReferenceLabel } from "./formatVariantReferenceLabel";
3233
import { productVariantCacheManager } from "./productVariantCache";
3334

3435
type AtributesOfFiles = Pick<AttributeValueInput, "file" | "id" | "values" | "contentType">;
@@ -566,7 +567,7 @@ const findProductVariantReference = (
566567

567568
if (variant) {
568569
return {
569-
label: `${product.name} ${variant.name}`,
570+
label: formatVariantReferenceLabel(product.name, variant.name),
570571
value: valueId,
571572
};
572573
}
@@ -612,7 +613,9 @@ export const getReferenceAttributeDisplayData = (
612613
* and whenever the user assigns references in the dialog into useFormset data. */
613614
const meta = attribute.additionalData?.find(m => m.value === valueId);
614615

615-
if (meta) {
616+
// Skip labels that are just the raw id (common when assign metadata
617+
// never resolved). Search / saved values can still supply a name.
618+
if (meta?.label && meta.label !== meta.value) {
616619
return {
617620
label: meta.label,
618621
value: meta.value,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { formatVariantReferenceLabel } from "./formatVariantReferenceLabel";
2+
3+
describe("formatVariantReferenceLabel", () => {
4+
it("joins product and variant the same way saved values do", () => {
5+
// Arrange // Act // Assert
6+
expect(formatVariantReferenceLabel("White Plimsolls", "44 / White")).toBe(
7+
"White Plimsolls: 44 / White",
8+
);
9+
});
10+
11+
it("returns the side that exists when the other is empty", () => {
12+
// Arrange // Act // Assert
13+
expect(formatVariantReferenceLabel("Bottle", "")).toBe("Bottle");
14+
expect(formatVariantReferenceLabel("", "700ml")).toBe("700ml");
15+
});
16+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/** Same shape as saved variant-reference attribute values (`Product: Variant`). */
2+
export const formatVariantReferenceLabel = (productName: string, variantName: string): string => {
3+
const product = productName.trim();
4+
const variant = variantName.trim();
5+
6+
if (!variant) {
7+
return product;
8+
}
9+
10+
if (!product) {
11+
return variant;
12+
}
13+
14+
return `${product}: ${variant}`;
15+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.root {
2+
display: inline-flex;
3+
align-items: center;
4+
height: var(--mu-spacing-8);
5+
line-height: 0;
6+
}
7+
8+
/* Flatten the trigger box so the <a> is the only flex item and tab stop. */
9+
.root :global(button) {
10+
display: contents;
11+
}
12+
13+
.icon {
14+
display: flex;
15+
align-items: center;
16+
justify-content: center;
17+
height: 100%;
18+
color: var(--mu-colors-text-default2);
19+
line-height: 0;
20+
}
21+
22+
.icon svg {
23+
display: block;
24+
}
25+
26+
.icon:hover {
27+
color: var(--mu-colors-text-default1);
28+
}
29+
30+
.icon:focus-visible {
31+
outline: 2px solid var(--mu-colors-border-accent1);
32+
outline-offset: 2px;
33+
border-radius: 4px;
34+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { render, screen } from "@testing-library/react";
2+
import userEvent from "@testing-library/user-event";
3+
4+
import { ContextualHelpIcon } from "./ContextualHelpIcon";
5+
6+
const trackEvent = jest.fn();
7+
8+
jest.mock("@dashboard/components/ProductAnalytics/useAnalytics", () => ({
9+
useAnalytics: () => ({ trackEvent }),
10+
}));
11+
12+
describe("ContextualHelpIcon", () => {
13+
beforeEach(() => {
14+
trackEvent.mockClear();
15+
});
16+
17+
it("renders a docs link and tracks the click", async () => {
18+
// Arrange
19+
const user = userEvent.setup();
20+
21+
render(
22+
<ContextualHelpIcon
23+
href="https://docs.saleor.io/developer/products/configuration"
24+
label="Learn more about product configurations"
25+
analyticsType="product_configuration_docs"
26+
dataTestId="product-configurations-docs"
27+
/>,
28+
);
29+
30+
// Act
31+
const link = screen.getByRole("link", { name: "Learn more about product configurations" });
32+
33+
await user.click(link);
34+
35+
// Assert
36+
expect(link).toHaveAttribute("href", "https://docs.saleor.io/developer/products/configuration");
37+
expect(trackEvent).toHaveBeenCalledWith("contextual_link_clicked", {
38+
type: "product_configuration_docs",
39+
});
40+
});
41+
42+
it("exposes the docs control as a link, not a nested button", () => {
43+
// Arrange & Act
44+
render(
45+
<ContextualHelpIcon
46+
href="https://docs.saleor.io/developer/products/configuration"
47+
label="Learn more about product configurations"
48+
analyticsType="product_configuration_docs"
49+
/>,
50+
);
51+
52+
// Assert
53+
expect(
54+
screen.getByRole("link", { name: "Learn more about product configurations" }),
55+
).toBeInTheDocument();
56+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
57+
});
58+
});

0 commit comments

Comments
 (0)