Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/itchy-frogs-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---

Attribute configuration: allow users to filter attribute values by slug or name when there are more than 5 values.
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changeset description states that users can filter "when there are more than 5 values", but this condition is not implemented in the code. The search feature is always available regardless of the number of values. Either update this description to match the actual implementation, or implement the condition as described.

Copilot uses AI. Check for mistakes.
8 changes: 8 additions & 0 deletions locale/defaultMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2026,6 +2026,10 @@
"context": "section header",
"string": "Product Attributes"
},
"9seX5T": {
"context": "attribute values search placeholder",
"string": "Search attribute values..."
},
"9uNz+T": {
"context": "button",
"string": "Cancel"
Expand Down Expand Up @@ -9176,6 +9180,10 @@
"context": "order history message",
"string": "Items were oversold"
},
"oegjWf": {
"context": "attribute values list: no search results",
"string": "No values match your search"
},
"of/+iV": {
"context": "transaction event type, refund was reversed, funds are back to store account",
"string": "Refund reversed"
Expand Down
8 changes: 7 additions & 1 deletion src/attributes/components/AttributePage/AttributePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import AttributeDetails from "../AttributeDetails";
import AttributeOrganization from "../AttributeOrganization";
import AttributeProperties from "../AttributeProperties";
import { AttributeReferenceTypesSection } from "../AttributeReferenceTypesSection/AttributeReferenceTypesSection";
import AttributeValues from "../AttributeValues";
import { AttributeValues } from "../AttributeValues/AttributeValues";

interface AttributePageProps {
attribute?: AttributeDetailsQuery["attribute"] | null | undefined;
Expand All @@ -80,6 +80,8 @@ interface AttributePageProps {
};
onNextPage: () => void;
onPreviousPage: () => void;
searchQuery?: string;
onSearchChange?: (query: string) => void;
children: (data: AttributePageFormData) => React.ReactNode;
}

Expand Down Expand Up @@ -119,6 +121,8 @@ const AttributePage = ({
pageInfo,
onNextPage,
onPreviousPage,
searchQuery,
onSearchChange,
children,
}: AttributePageProps) => {
const intl = useIntl();
Expand Down Expand Up @@ -291,6 +295,8 @@ const AttributePage = ({
pageInfo={pageInfo}
onNextPage={onNextPage}
onPreviousPage={onPreviousPage}
searchQuery={searchQuery}
onSearchChange={onSearchChange}
/>
</>
)}
Expand Down
247 changes: 148 additions & 99 deletions src/attributes/components/AttributeValues/AttributeValues.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { rippleAttributeValuesSearch } from "@dashboard/attributes/ripples/attributeValuesSearch";
import { DashboardCard } from "@dashboard/components/Card";
import { iconSize, iconStrokeWidthBySize } from "@dashboard/components/icons";
import { Placeholder } from "@dashboard/components/Placeholder";
import { ResponsiveTable } from "@dashboard/components/ResponsiveTable";
import { SearchInput } from "@dashboard/components/SearchInput/SearchInput";
import { SortableTableBody, SortableTableRow } from "@dashboard/components/SortableTable";
import { TablePagination } from "@dashboard/components/TablePagination";
import TableRowLink from "@dashboard/components/TableRowLink";
Expand All @@ -11,6 +13,7 @@ import {
AttributeValueListFragment,
} from "@dashboard/graphql";
import { renderCollection, stopPropagation } from "@dashboard/misc";
import { Ripple } from "@dashboard/ripples/components/Ripple";
import { ListProps, PaginateListProps, RelayToFlat, ReorderAction } from "@dashboard/types";
import { TableCell, TableHead } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
Expand All @@ -28,6 +31,8 @@ interface AttributeValuesProps
onValueReorder: ReorderAction;
onValueUpdate: (id: string) => void;
inputType: AttributeInputTypeEnum;
searchQuery?: string;
onSearchChange?: (query: string) => void;
}

const useStyles = makeStyles(
Expand Down Expand Up @@ -72,6 +77,7 @@ const getSwatchCellStyle = (value?: AttributeValueFragment | undefined) => {
? { backgroundImage: `url(${value.file.url})` }
: { backgroundColor: value.value ?? undefined };
};

const AttributeValues = ({
disabled,
onValueAdd,
Expand All @@ -85,11 +91,16 @@ const AttributeValues = ({
onNextPage,
onPreviousPage,
inputType,
searchQuery = "",
onSearchChange,
}: AttributeValuesProps) => {
const classes = useStyles({});
const intl = useIntl();
const isSwatch = inputType === AttributeInputTypeEnum.SWATCH;

// Show search when callback is provided (controlled by parent)
const showSearch = Boolean(onSearchChange);
Comment on lines +101 to +102
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description states "Search only appears when there are more than 5 values to avoid UI clutter", but this implementation shows the search whenever onSearchChange is provided, regardless of the number of values. The condition should check the total count of attribute values and only enable search when it exceeds 5 values. Consider adding logic like: const showSearch = Boolean(onSearchChange) && (values?.length > 5 || searchQuery); to match the PR description requirement.

Copilot uses AI. Check for mistakes.

return (
<DashboardCard data-test-id="attribute-values-section">
<DashboardCard.Header>
Expand Down Expand Up @@ -118,113 +129,151 @@ const AttributeValues = ({
<DashboardCard.Content>
{values === undefined ? (
<Skeleton />
) : values.length === 0 ? (
<Placeholder>
<FormattedMessage
id="dAst+b"
defaultMessage="No values found"
description="attribute values list: no attribute values found"
/>
</Placeholder>
) : (
<ResponsiveTable
footer={
<TablePagination
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage}
hasPreviousPage={pageInfo && !disabled ? pageInfo.hasPreviousPage : false}
onPreviousPage={onPreviousPage}
settings={settings}
onUpdateListSettings={onUpdateListSettings}
/>
}
>
<TableHead>
<TableRowLink>
<TableCell className={classes.columnDrag} />
{isSwatch && (
<TableCell className={classes.columnSwatch}>
<FormattedMessage
id="NUevU9"
defaultMessage="Swatch"
description="attribute values list: slug column header"
/>
</TableCell>
)}
<TableCell className={classes.columnAdmin}>
<FormattedMessage
id="3psvRS"
defaultMessage="Admin"
description="attribute values list: slug column header"
/>
</TableCell>
<TableCell className={classes.columnStore}>
<FormattedMessage
id="H60H6L"
defaultMessage="Default Store View"
description="attribute values list: name column header"
<Box display="flex" flexDirection="column" gap={4}>
{/* Search input - always visible when search is enabled */}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "always visible when search is enabled" is misleading given the PR description states search should only appear when there are more than 5 values. Either update this comment to reflect the intended behavior, or implement the 5-value threshold as described in the PR.

Suggested change
{/* Search input - always visible when search is enabled */}
{/* Search input - rendered when onSearchChange is provided (visibility controlled by parent) */}

Copilot uses AI. Check for mistakes.
{showSearch && onSearchChange && (
<Box position="relative">
<SearchInput
value={searchQuery}
onChange={onSearchChange}
placeholder={intl.formatMessage({
id: "9seX5T",
defaultMessage: "Search attribute values...",
description: "attribute values search placeholder",
})}
data-test-id="attribute-value-search-input"
/>
<Box position="absolute" __top="-4px" __right="-4px">
<Ripple model={rippleAttributeValuesSearch} />
</Box>
</Box>
)}
{/* No values at all (not searching) */}
{values.length === 0 && !searchQuery ? (
<Placeholder>
<FormattedMessage
id="dAst+b"
defaultMessage="No values found"
description="attribute values list: no attribute values found"
/>
</Placeholder>
) : /* Search returned no results */
values.length === 0 && searchQuery ? (
<Placeholder>
<FormattedMessage
id="oegjWf"
defaultMessage="No values match your search"
description="attribute values list: no search results"
/>
</Placeholder>
) : (
<ResponsiveTable
footer={
<TablePagination
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage}
hasPreviousPage={pageInfo && !disabled ? pageInfo.hasPreviousPage : false}
onPreviousPage={onPreviousPage}
settings={settings}
onUpdateListSettings={onUpdateListSettings}
/>
</TableCell>
<TableCell className={classes.iconCell} />
</TableRowLink>
</TableHead>
<SortableTableBody onSortEnd={onValueReorder}>
{renderCollection(values, (value, valueIndex) => (
<SortableTableRow<"row">
data-test-id="attributes-rows"
className={value ? classes.link : undefined}
hover={!!value}
onClick={value ? () => onValueUpdate(value.id) : undefined}
key={value?.id}
index={valueIndex || 0}
>
{isSwatch && (
<TableCell className={classes.columnSwatch}>
{value?.file ? (
<Box
as="img"
objectFit="cover"
alt=""
src={value.file.url}
__width={32}
__height={32}
data-test-id="swatch-image"
}
>
<TableHead>
<TableRowLink>
<TableCell className={classes.columnDrag} />
{isSwatch && (
<TableCell className={classes.columnSwatch}>
<FormattedMessage
id="NUevU9"
defaultMessage="Swatch"
description="attribute values list: slug column header"
/>
) : (
<div
data-test-id="swatch-image"
className={classes.swatch}
style={getSwatchCellStyle(value)}
/>
)}
</TableCell>
)}
<TableCell className={classes.columnAdmin}>
<FormattedMessage
id="3psvRS"
defaultMessage="Admin"
description="attribute values list: slug column header"
/>
</TableCell>
<TableCell className={classes.columnStore}>
<FormattedMessage
id="H60H6L"
defaultMessage="Default Store View"
description="attribute values list: name column header"
/>
</TableCell>
)}
<TableCell className={classes.columnAdmin} data-test-id="attribute-value-name">
{value?.slug ?? <Skeleton />}
</TableCell>
<TableCell className={classes.columnStore}>
{value?.name ?? <Skeleton />}
</TableCell>
<TableCell className={classes.iconCell}>
<Button
icon={
<Trash2 size={iconSize.small} strokeWidth={iconStrokeWidthBySize.small} />
}
data-test-id="delete-attribute-value-button"
variant="secondary"
disabled={disabled}
onClick={stopPropagation(() => onValueDelete(value?.id ?? ""))}
/>
</TableCell>
</SortableTableRow>
))}
</SortableTableBody>
</ResponsiveTable>
<TableCell className={classes.iconCell} />
</TableRowLink>
</TableHead>
<SortableTableBody onSortEnd={onValueReorder} disabled={!!searchQuery}>
{renderCollection(values, (value, valueIndex) => (
<SortableTableRow<"row">
data-test-id="attributes-rows"
className={value ? classes.link : undefined}
hover={!!value}
onClick={value ? () => onValueUpdate(value.id) : undefined}
key={value?.id}
index={valueIndex || 0}
>
{isSwatch && (
<TableCell className={classes.columnSwatch}>
{value?.file ? (
<Box
as="img"
objectFit="cover"
alt=""
src={value.file.url}
__width={32}
__height={32}
data-test-id="swatch-image"
/>
) : (
<div
data-test-id="swatch-image"
className={classes.swatch}
style={getSwatchCellStyle(value)}
/>
)}
</TableCell>
)}
<TableCell
className={classes.columnAdmin}
data-test-id="attribute-value-name"
>
{value?.slug ?? <Skeleton />}
</TableCell>
<TableCell className={classes.columnStore}>
{value?.name ?? <Skeleton />}
</TableCell>
<TableCell className={classes.iconCell}>
<Button
icon={
<Trash2
size={iconSize.small}
strokeWidth={iconStrokeWidthBySize.small}
/>
}
data-test-id="delete-attribute-value-button"
variant="secondary"
disabled={disabled}
onClick={stopPropagation(() => onValueDelete(value?.id ?? ""))}
/>
</TableCell>
</SortableTableRow>
))}
</SortableTableBody>
</ResponsiveTable>
)}
</Box>
)}
</DashboardCard.Content>
</DashboardCard>
);
};

AttributeValues.displayName = "AttributeValues";
export default AttributeValues;
export { AttributeValues };
3 changes: 1 addition & 2 deletions src/attributes/components/AttributeValues/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { default } from "./AttributeValues";
export * from "./AttributeValues";
export { AttributeValues } from "./AttributeValues";
Loading
Loading