Experimental filters: add tests and fix wrong data send to core (#4039)

This commit is contained in:
Krzysztof Żuraw 2023-08-01 11:37:06 +02:00 committed by GitHub
parent 0995b02dfb
commit 32d1a5b8c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 413 additions and 27 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---
Experimental filters: add unit tests and fix wrong data send to core

View file

@ -28,13 +28,20 @@ export interface Handler {
}
export const createOptionsFromAPI = (
// TODO: try to use type from graphql
data: Array<{ node: { name: string | null; id: string; slug: string } }>,
data: Array<{
node: {
name: string | null;
id: string;
slug: string;
originalSlug?: string | null;
};
}>,
): ItemOption[] =>
data.map(({ node }) => ({
label: node.name ?? "",
value: node.id,
slug: node.slug,
originalSlug: node.originalSlug,
}));
export class AttributeChoicesHandler implements Handler {
@ -170,6 +177,6 @@ export class BooleanValuesHandler implements Handler {
constructor(public options: LeftOperand[]) {}
fetch = async (): Promise<LeftOperand[]> => {
return this.options
return this.options;
};
}

View file

@ -0,0 +1,83 @@
import { UrlEntry, UrlToken } from "../ValueProvider/UrlToken";
import { InitialStateResponse } from "./InitialStateResponse";
describe("ConditionalFilter / API / InitialStateResponse", () => {
it("should filter by dynamic attribute token", () => {
const initialState = InitialStateResponse.empty();
initialState.attribute = {
"attribute-1": {
choices: [
{ label: "Choice 1", value: "value-1", slug: "choice-1" },
{ label: "Choice 2", value: "value-2", slug: "choice-2" },
],
slug: "attribute-1",
value: "1",
label: "Attribute 1",
inputType: "DROPDOWN",
},
};
const token = UrlToken.fromUrlEntry(
new UrlEntry("o2.attribute-1", ["value-1"]),
);
const expectedOutput = [
{ label: "Choice 1", slug: "choice-1", value: "value-1" },
];
const result = initialState.filterByUrlToken(token);
expect(result).toEqual(expectedOutput);
});
it("should filter by static token type", () => {
const initialState = InitialStateResponse.empty();
initialState.category = [
{ label: "Category 1", value: "1", slug: "category-1" },
];
const token = UrlToken.fromUrlEntry(
new UrlEntry("s0.category-1", "category-1"),
);
const expectedOutput = ["category-1"];
const result = initialState.filterByUrlToken(token);
expect(result).toEqual(expectedOutput);
});
it("should filter by boolean attribute token", () => {
const initialState = InitialStateResponse.empty();
initialState.attribute = {
"attribute-2": {
choices: [
{ label: "True", value: "true", slug: "true" },
{ label: "False", value: "false", slug: "false" },
],
slug: "attribute-2",
value: "2",
label: "Attribute 2",
inputType: "BOOLEAN",
},
};
const token = UrlToken.fromUrlEntry(new UrlEntry("b0.attribute-2", "true"));
const expectedOutput = {
label: "Yes",
slug: "true",
type: "BOOLEAN",
value: "true",
};
const result = initialState.filterByUrlToken(token);
expect(result).toEqual(expectedOutput);
});
it("should filter by static attribute token", () => {
const initialState = InitialStateResponse.empty();
initialState.attribute = {
size: {
value: "",
label: "Size",
slug: "size",
inputType: "NUMERIC",
choices: [],
},
};
const token = UrlToken.fromUrlEntry(new UrlEntry("n0.size", "123"));
const result = initialState.filterByUrlToken(token);
expect(result).toEqual("123");
});
});

View file

@ -6,7 +6,12 @@ import { ItemOption } from "../FilterElement/ConditionValue";
import { UrlToken } from "../ValueProvider/UrlToken";
export interface AttributeDTO {
choices: Array<{ label: string; value: string; slug: string }>;
choices: Array<{
label: string;
value: string;
slug: string;
originalSlug?: string;
}>;
inputType: AttributeInputType;
label: string;
slug: string;
@ -69,12 +74,12 @@ export class InitialStateResponse implements InitialState {
return [token.value] as string[];
}
return this.getEntryByname(token.name).filter(
return this.getEntryByName(token.name).filter(
({ slug }) => slug && token.value.includes(slug),
);
}
private getEntryByname(name: string) {
private getEntryByName(name: string) {
switch (name) {
case "category":
return this.category;

View file

@ -0,0 +1,167 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConditionalFilter / API / createInitialStateFromData should create initial state from queries 1`] = `
Object {
"attribute": Object {
"attribute-1": Object {
"choices": Array [
Object {
"label": "Choice 1",
"originalSlug": undefined,
"slug": "choice-1",
"value": "1",
},
Object {
"label": "Choice 2",
"originalSlug": undefined,
"slug": "choice-2",
"value": "2",
},
],
"inputType": "MULTISELECT",
"label": "Attribute 1",
"slug": "attribute-1",
"value": "1",
},
"attribute-2": Object {
"choices": Array [
Object {
"label": "Yes",
"slug": "true",
"type": undefined,
"value": "true",
},
Object {
"label": "No",
"slug": "false",
"type": undefined,
"value": "false",
},
],
"inputType": "BOOLEAN",
"label": "Attribute 2",
"slug": "attribute-2",
"value": "2",
},
},
"category": Array [
Object {
"label": "Category 1",
"originalSlug": undefined,
"slug": "category-1",
"value": "1",
},
Object {
"label": "Category 2",
"originalSlug": undefined,
"slug": "category-2",
"value": "2",
},
],
"channel": Array [
Object {
"label": "Channel 1",
"slug": "channel-1",
"value": "1",
},
],
"collection": Array [
Object {
"label": "Collection 1",
"originalSlug": undefined,
"slug": "collection-1",
"value": "1",
},
Object {
"label": "Collection 2",
"originalSlug": undefined,
"slug": "collection-2",
"value": "2",
},
],
"giftCard": Array [
Object {
"label": "Yes",
"slug": "true",
"type": undefined,
"value": "true",
},
Object {
"label": "No",
"slug": "false",
"type": undefined,
"value": "false",
},
],
"hasCategory": Array [
Object {
"label": "Yes",
"slug": "true",
"type": undefined,
"value": "true",
},
Object {
"label": "No",
"slug": "false",
"type": undefined,
"value": "false",
},
],
"isAvailable": Array [
Object {
"label": "Yes",
"slug": "true",
"type": undefined,
"value": "true",
},
Object {
"label": "No",
"slug": "false",
"type": undefined,
"value": "false",
},
],
"isPublished": Array [
Object {
"label": "Yes",
"slug": "true",
"type": undefined,
"value": "true",
},
Object {
"label": "No",
"slug": "false",
"type": undefined,
"value": "false",
},
],
"isVisibleInListing": Array [
Object {
"label": "Yes",
"slug": "true",
"type": undefined,
"value": "true",
},
Object {
"label": "No",
"slug": "false",
"type": undefined,
"value": "false",
},
],
"productType": Array [
Object {
"label": "Product Type 1",
"originalSlug": undefined,
"slug": "product-type-1",
"value": "1",
},
Object {
"label": "Product Type 2",
"originalSlug": undefined,
"slug": "product-type-2",
"value": "2",
},
],
}
`;

View file

@ -0,0 +1,111 @@
import { ApolloQueryResult } from "@apollo/client";
import {
_GetChannelOperandsQuery,
_SearchAttributeOperandsQuery,
_SearchCategoriesOperandsQuery,
_SearchCollectionsOperandsQuery,
_SearchProductTypesOperandsQuery,
} from "@dashboard/graphql";
import { createInitialStateFromData } from "./helpers";
describe("ConditionalFilter / API / createInitialStateFromData", () => {
const channelQuery = {
data: {
channels: [
{ id: "1", name: "Channel 1", slug: "channel-1" },
{ id: "2", name: "Channel 2", slug: "channel-2" },
],
},
} as ApolloQueryResult<_GetChannelOperandsQuery>;
const collectionQuery = {
data: {
collections: {
edges: [
{ node: { id: "1", name: "Collection 1", slug: "collection-1" } },
{ node: { id: "2", name: "Collection 2", slug: "collection-2" } },
],
},
},
} as ApolloQueryResult<_SearchCollectionsOperandsQuery>;
const categoryQuery = {
data: {
categories: {
edges: [
{ node: { id: "1", name: "Category 1", slug: "category-1" } },
{ node: { id: "2", name: "Category 2", slug: "category-2" } },
],
},
},
} as ApolloQueryResult<_SearchCategoriesOperandsQuery>;
const productTypeQuery = {
data: {
productTypes: {
edges: [
{ node: { id: "1", name: "Product Type 1", slug: "product-type-1" } },
{ node: { id: "2", name: "Product Type 2", slug: "product-type-2" } },
],
},
},
} as ApolloQueryResult<_SearchProductTypesOperandsQuery>;
const attributeQuery = {
data: {
attributes: {
edges: [
{
node: {
id: "1",
name: "Attribute 1",
slug: "attribute-1",
inputType: "MULTISELECT",
choices: {
edges: [
{
node: {
id: "1",
name: "Choice 1",
slug: "choice-1",
},
},
{
node: {
id: "2",
name: "Choice 2",
slug: "choice-2",
},
},
],
},
},
},
{
node: {
id: "2",
name: "Attribute 2",
slug: "attribute-2",
inputType: "BOOLEAN",
},
},
],
},
},
} as ApolloQueryResult<_SearchAttributeOperandsQuery>;
it("should create initial state from queries", () => {
const data = [
channelQuery,
collectionQuery,
categoryQuery,
productTypeQuery,
attributeQuery,
];
const channel = ["channel-1"];
const result = createInitialStateFromData(data, channel);
expect(result).toMatchSnapshot();
});
});

View file

@ -114,6 +114,7 @@ export const initialDynamicOperands = gql`
slug: id
id
name
originalSlug: slug
}
}
}
@ -132,6 +133,7 @@ export const dynamicOperandsQueries = gql`
slug: id
id
name
originalSlug: slug
}
}
}

View file

@ -11,18 +11,18 @@ export const ConditionalFilters: FC<{ onClose: () => void }> = ({
onClose,
}) => {
const { valueProvider, containerState } = useConditionalFilterContext();
const [errors, setErrors] = useState<ErrorEntry[]>([])
const [errors, setErrors] = useState<ErrorEntry[]>([]);
const handleConfirm = (value: FilterContainer) => {
const validator = new Validator(value)
const validator = new Validator(value);
if (validator.isValid()) {
valueProvider.persist(value);
onClose();
return
return;
}
setErrors(validator.getErrors())
setErrors(validator.getErrors());
};
const handleCancel = () => {
@ -34,13 +34,12 @@ export const ConditionalFilters: FC<{ onClose: () => void }> = ({
return valueProvider.loading ? (
<LoadingFiltersArea />
) : (
<Box
padding={3}
backgroundColor="interactiveNeutralSecondaryHovering"
borderBottomLeftRadius={2}
borderBottomRightRadius={2}
>
<FiltersArea onConfirm={handleConfirm} errors={errors} onCancel={handleCancel} />
<Box padding={3} borderBottomLeftRadius={2} borderBottomRightRadius={2}>
<FiltersArea
onConfirm={handleConfirm}
errors={errors}
onCancel={handleCancel}
/>
</Box>
);
};

View file

@ -2,6 +2,8 @@ export interface ItemOption {
label: string;
value: string;
slug: string;
// TODO: remove this when https://github.com/saleor/saleor/issues/13076 is ready
originalSlug?: string | null;
}
export type ConditionValue =
@ -11,16 +13,16 @@ export type ConditionValue =
| string[]
| [string, string];
export const isItemOption = (x: ConditionValue): x is ItemOption =>
typeof x === "object" && "value" in x
typeof x === "object" && "value" in x;
export const isItemOptionArray = (x: ConditionValue): x is ItemOption[] =>
Array.isArray(x) && (x as ItemOption[]).every(isItemOption)
Array.isArray(x) && (x as ItemOption[]).every(isItemOption);
export const isTuple = (x: ConditionValue): x is [string, string] =>
Array.isArray(x) && x.length === 2 && (x as string[]).every(y => typeof y === "string")
Array.isArray(x) &&
x.length === 2 &&
(x as string[]).every(y => typeof y === "string");
export const slugFromConditionValue = (
rawEntry: ConditionValue,

View file

@ -112,11 +112,14 @@ const createAttributeQueryPart = (
}
if (isItemOption(value)) {
return { slug: attributeSlug, values: [value.value] };
return { slug: attributeSlug, values: [value.originalSlug || value.value] };
}
if (isItemOptionArray(value)) {
return { slug: attributeSlug, values: value.map(x => x.value) };
return {
slug: attributeSlug,
values: value.map(x => x.originalSlug || x.value),
};
}
if (typeof value === "string") {

View file

@ -5687,6 +5687,7 @@ export const _SearchAttributeOperandsDocument = gql`
slug: id
id
name
originalSlug: slug
}
}
}
@ -5734,6 +5735,7 @@ export const _GetAttributeChoicesDocument = gql`
slug: id
id
name
originalSlug: slug
}
}
}

View file

@ -8409,7 +8409,7 @@ export type _SearchAttributeOperandsQueryVariables = Exact<{
}>;
export type _SearchAttributeOperandsQuery = { __typename: 'Query', attributes: { __typename: 'AttributeCountableConnection', edges: Array<{ __typename: 'AttributeCountableEdge', node: { __typename: 'Attribute', id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, choices: { __typename: 'AttributeValueCountableConnection', edges: Array<{ __typename: 'AttributeValueCountableEdge', node: { __typename: 'AttributeValue', id: string, name: string | null, slug: string } }> } | null } }> } | null };
export type _SearchAttributeOperandsQuery = { __typename: 'Query', attributes: { __typename: 'AttributeCountableConnection', edges: Array<{ __typename: 'AttributeCountableEdge', node: { __typename: 'Attribute', id: string, name: string | null, slug: string | null, inputType: AttributeInputTypeEnum | null, choices: { __typename: 'AttributeValueCountableConnection', edges: Array<{ __typename: 'AttributeValueCountableEdge', node: { __typename: 'AttributeValue', id: string, name: string | null, slug: string, originalSlug: string | null } }> } | null } }> } | null };
export type _GetAttributeChoicesQueryVariables = Exact<{
slug: Scalars['String'];
@ -8418,7 +8418,7 @@ export type _GetAttributeChoicesQueryVariables = Exact<{
}>;
export type _GetAttributeChoicesQuery = { __typename: 'Query', attribute: { __typename: 'Attribute', choices: { __typename: 'AttributeValueCountableConnection', edges: Array<{ __typename: 'AttributeValueCountableEdge', node: { __typename: 'AttributeValue', id: string, name: string | null, slug: string } }> } | null } | null };
export type _GetAttributeChoicesQuery = { __typename: 'Query', attribute: { __typename: 'Attribute', choices: { __typename: 'AttributeValueCountableConnection', edges: Array<{ __typename: 'AttributeValueCountableEdge', node: { __typename: 'AttributeValue', id: string, name: string | null, slug: string, originalSlug: string | null } }> } | null } | null };
export type _GetCollectionsChoicesQueryVariables = Exact<{
first: Scalars['Int'];

View file

@ -66,7 +66,7 @@ export function getSortQueryVariables(
}
const field = getSortQueryField(params.sort);
// TODO: how to handle search & sort
// TODO: apply fix after https://github.com/saleor/saleor/issues/13557 is done
return {
direction,
field,