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 = ( export const createOptionsFromAPI = (
// TODO: try to use type from graphql data: Array<{
data: Array<{ node: { name: string | null; id: string; slug: string } }>, node: {
name: string | null;
id: string;
slug: string;
originalSlug?: string | null;
};
}>,
): ItemOption[] => ): ItemOption[] =>
data.map(({ node }) => ({ data.map(({ node }) => ({
label: node.name ?? "", label: node.name ?? "",
value: node.id, value: node.id,
slug: node.slug, slug: node.slug,
originalSlug: node.originalSlug,
})); }));
export class AttributeChoicesHandler implements Handler { export class AttributeChoicesHandler implements Handler {
@ -170,6 +177,6 @@ export class BooleanValuesHandler implements Handler {
constructor(public options: LeftOperand[]) {} constructor(public options: LeftOperand[]) {}
fetch = async (): Promise<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"; import { UrlToken } from "../ValueProvider/UrlToken";
export interface AttributeDTO { export interface AttributeDTO {
choices: Array<{ label: string; value: string; slug: string }>; choices: Array<{
label: string;
value: string;
slug: string;
originalSlug?: string;
}>;
inputType: AttributeInputType; inputType: AttributeInputType;
label: string; label: string;
slug: string; slug: string;
@ -69,12 +74,12 @@ export class InitialStateResponse implements InitialState {
return [token.value] as string[]; return [token.value] as string[];
} }
return this.getEntryByname(token.name).filter( return this.getEntryByName(token.name).filter(
({ slug }) => slug && token.value.includes(slug), ({ slug }) => slug && token.value.includes(slug),
); );
} }
private getEntryByname(name: string) { private getEntryByName(name: string) {
switch (name) { switch (name) {
case "category": case "category":
return this.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 slug: id
id id
name name
originalSlug: slug
} }
} }
} }
@ -132,6 +133,7 @@ export const dynamicOperandsQueries = gql`
slug: id slug: id
id id
name name
originalSlug: slug
} }
} }
} }

View file

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

View file

@ -2,6 +2,8 @@ export interface ItemOption {
label: string; label: string;
value: string; value: string;
slug: string; slug: string;
// TODO: remove this when https://github.com/saleor/saleor/issues/13076 is ready
originalSlug?: string | null;
} }
export type ConditionValue = export type ConditionValue =
@ -11,16 +13,16 @@ export type ConditionValue =
| string[] | string[]
| [string, string]; | [string, string];
export const isItemOption = (x: ConditionValue): x is ItemOption => 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[] => 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] => 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 = ( export const slugFromConditionValue = (
rawEntry: ConditionValue, rawEntry: ConditionValue,

View file

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

View file

@ -5687,6 +5687,7 @@ export const _SearchAttributeOperandsDocument = gql`
slug: id slug: id
id id
name name
originalSlug: slug
} }
} }
} }
@ -5734,6 +5735,7 @@ export const _GetAttributeChoicesDocument = gql`
slug: id slug: id
id id
name 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<{ export type _GetAttributeChoicesQueryVariables = Exact<{
slug: Scalars['String']; 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<{ export type _GetCollectionsChoicesQueryVariables = Exact<{
first: Scalars['Int']; first: Scalars['Int'];

View file

@ -66,7 +66,7 @@ export function getSortQueryVariables(
} }
const field = getSortQueryField(params.sort); 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 { return {
direction, direction,
field, field,