Experimental filters: add tests and fix wrong data send to core (#4039)
This commit is contained in:
parent
0995b02dfb
commit
32d1a5b8c5
13 changed files with 413 additions and 27 deletions
5
.changeset/smart-gifts-applaud.md
Normal file
5
.changeset/smart-gifts-applaud.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-dashboard": patch
|
||||
---
|
||||
|
||||
Experimental filters: add unit tests and fix wrong data send to core
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue