Experimental filters: refactor API hooks (#3882)

This commit is contained in:
Krzysztof Żuraw 2023-07-10 08:17:30 +02:00 committed by GitHub
parent b09f2d556f
commit cc0e0e58f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 206 additions and 195 deletions

View file

@ -0,0 +1,7 @@
---
"saleor-dashboard": patch
---
Experimental filters: refactor API hooks.
This PR refactors API hooks used to fetch data. Right now they return the provider which then is used by the filter container to update options coming from API.

View file

@ -83,7 +83,7 @@
"@typescript-eslint/ban-ts-comment": "warn",
"@typescript-eslint/ban-types": "warn",
"@typescript-eslint/consistent-type-assertions": "warn",
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/naming-convention": "warn",
"@typescript-eslint/no-base-to-string": "warn",
"@typescript-eslint/no-dynamic-delete": "warn",

14
package-lock.json generated
View file

@ -27,7 +27,7 @@
"@material-ui/lab": "^4.0.0-alpha.61",
"@material-ui/styles": "^4.11.4",
"@reach/auto-id": "^0.16.0",
"@saleor/macaw-ui": "0.8.0-pre.103",
"@saleor/macaw-ui": "0.8.0-pre.104",
"@saleor/sdk": "0.6.0",
"@sentry/react": "^6.0.0",
"@types/faker": "^5.1.6",
@ -7947,9 +7947,9 @@
}
},
"node_modules/@saleor/macaw-ui": {
"version": "0.8.0-pre.103",
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.103.tgz",
"integrity": "sha512-Y6E4vHKhXf5cRZiQwLOy6LnabUH1FkyDQ9GZ03RH2PzXwoVA972v21ySJV4Vd3Kk8c7X5ugGzgV0Etovo963lA==",
"version": "0.8.0-pre.104",
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.104.tgz",
"integrity": "sha512-5Et/UIsH6ZzVyJLMhhnbN5PDllRGhyhJXBXUlgOdD0yH47+PPeaN9BPDDrlX/eiJbLMimJOIuj5hhb+WhWT/Zw==",
"dependencies": {
"@dessert-box/react": "^0.4.0",
"@floating-ui/react-dom-interactions": "^0.5.0",
@ -41209,9 +41209,9 @@
}
},
"@saleor/macaw-ui": {
"version": "0.8.0-pre.103",
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.103.tgz",
"integrity": "sha512-Y6E4vHKhXf5cRZiQwLOy6LnabUH1FkyDQ9GZ03RH2PzXwoVA972v21ySJV4Vd3Kk8c7X5ugGzgV0Etovo963lA==",
"version": "0.8.0-pre.104",
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.104.tgz",
"integrity": "sha512-5Et/UIsH6ZzVyJLMhhnbN5PDllRGhyhJXBXUlgOdD0yH47+PPeaN9BPDDrlX/eiJbLMimJOIuj5hhb+WhWT/Zw==",
"requires": {
"@dessert-box/react": "^0.4.0",
"@floating-ui/react-dom-interactions": "^0.5.0",

View file

@ -34,7 +34,7 @@
"@material-ui/lab": "^4.0.0-alpha.61",
"@material-ui/styles": "^4.11.4",
"@reach/auto-id": "^0.16.0",
"@saleor/macaw-ui": "0.8.0-pre.103",
"@saleor/macaw-ui": "0.8.0-pre.104",
"@saleor/sdk": "0.6.0",
"@sentry/react": "^6.0.0",
"@types/faker": "^5.1.6",

View file

@ -0,0 +1,12 @@
import { FilterContainer } from "../FilterElement";
import { ItemOption } from "../FilterElement/ConditionValue";
import { LeftOperand } from "../LeftOperandsProvider";
export interface FilterAPIProvider {
fetchRightOptions: (
position: string,
value: FilterContainer,
inputValue: string,
) => Promise<ItemOption[]>;
fetchLeftOptions: (inputValue: string) => Promise<LeftOperand[]>;
}

View file

@ -21,7 +21,7 @@ import {
} from "@dashboard/graphql";
import { ItemOption } from "../FilterElement/ConditionValue";
import { LeftOperand } from "../LeftOperandsProvider";
export interface Handler {
client: ApolloClient<unknown>;
@ -146,7 +146,7 @@ export class ChannelHandler implements Handler {
export class AttributesHandler implements Handler {
constructor(public client: ApolloClient<unknown>, public query: string) {}
fetch = async () => {
fetch = async (): Promise<LeftOperand[]> => {
const { data } = await this.client.query<
_GetDynamicLeftOperandsQuery,
_GetDynamicLeftOperandsQueryVariables
@ -161,7 +161,7 @@ export class AttributesHandler implements Handler {
data.attributes?.edges.map(({ node }) => ({
label: node.name ?? "",
value: node.id,
type: node.inputType,
type: node.inputType ?? ("" as LeftOperand["type"]),
slug: node.slug ?? "",
})) ?? []
);

View file

@ -0,0 +1,86 @@
import { ApolloClient, useApolloClient } from "@apollo/client";
import { FilterContainer, FilterElement } from "../FilterElement";
import { FilterAPIProvider } from "./FilterAPIProvider";
import {
AttributeChoicesHandler,
AttributesHandler,
CategoryHandler,
ChannelHandler,
CollectionHandler,
Handler,
ProductTypeHandler,
} from "./Handler";
const getFilterElement = (
value: FilterContainer,
index: number,
): FilterElement => {
const possibleFilterElement = value[index];
if (
typeof possibleFilterElement !== "string" &&
!Array.isArray(possibleFilterElement)
) {
return possibleFilterElement;
}
throw new Error("Unknown filter element used to create API handler");
};
const createAPIHandler = (
selectedRow: FilterElement,
client: ApolloClient<unknown>,
inputValue: string,
): Handler => {
if (selectedRow.isAttribute()) {
return new AttributeChoicesHandler(
client,
selectedRow.value.value,
inputValue,
);
}
if (selectedRow.isCollection()) {
return new CollectionHandler(client, inputValue);
}
if (selectedRow.isCategory()) {
return new CategoryHandler(client, inputValue);
}
if (selectedRow.isProductType()) {
return new ProductTypeHandler(client, inputValue);
}
if (selectedRow.isChannel()) {
return new ChannelHandler(client, inputValue);
}
throw new Error("Unknown filter element");
};
export const useProductFilterAPIProvider = (): FilterAPIProvider => {
const client = useApolloClient();
const fetchRightOptions = async (
position: string,
value: FilterContainer,
inputValue: string,
) => {
const index = parseInt(position, 10);
const filterElement = getFilterElement(value, index);
const handler = createAPIHandler(filterElement, client, inputValue);
return handler.fetch();
};
const fetchLeftOptions = async (inputValue: string) => {
const handler = new AttributesHandler(client, inputValue);
return handler.fetch();
};
return {
fetchRightOptions,
fetchLeftOptions,
};
};

View file

@ -1,84 +0,0 @@
import { ApolloClient } from "@apollo/client";
import { FilterContainer, FilterElement } from "../FilterElement";
import {
AttributeChoicesHandler,
AttributesHandler,
CategoryHandler,
ChannelHandler,
CollectionHandler,
Handler,
ProductTypeHandler,
} from "./Handler";
const getFilterElement = (value: any, index: number): FilterElement => {
const possibleFilterElement = value[index];
return typeof possibleFilterElement !== "string"
? possibleFilterElement
: null;
};
const createAPIHandler = (
selectedRow: FilterElement,
client: ApolloClient<unknown>,
inputValue: string,
): Handler => {
if (selectedRow.isAttribute()) {
return new AttributeChoicesHandler(
client,
selectedRow.value.value,
inputValue,
);
}
if (selectedRow.isCollection()) {
return new CollectionHandler(client, inputValue);
}
if (selectedRow.isCategory()) {
return new CategoryHandler(client, inputValue);
}
if (selectedRow.isProductType()) {
return new ProductTypeHandler(client, inputValue);
}
if (selectedRow.isChannel()) {
return new ChannelHandler(client, inputValue);
}
throw new Error("Unknown filter element");
};
export const getInitialRightOperatorOptions = async (
client: ApolloClient<unknown>,
position: string,
value: FilterContainer,
) => {
const index = parseInt(position, 10);
const filterElement = getFilterElement(value, index);
const handler = createAPIHandler(filterElement, client, "");
return handler.fetch();
};
export const getRightOperatorOptionsByQuery = async (
client: ApolloClient<unknown>,
position: string,
value: FilterContainer,
inputValue: string,
) => {
const index = parseInt(position, 10);
const filterElement = getFilterElement(value, index);
const handler = createAPIHandler(filterElement, client, inputValue);
return handler.fetch();
};
export const getLeftOperatorOptions = async (
client: ApolloClient<unknown>,
inputValue: string,
) => {
const handler = new AttributesHandler(client, inputValue);
return handler.fetch();
};

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/member-ordering */
import { InitialStateResponse } from "../API/InitialStateResponse";
import { LeftOperand } from "./../useLeftOperands";
import { LeftOperand } from "../LeftOperandsProvider";
import { UrlToken } from "./../ValueProvider/UrlToken";
import { ConditionOptions } from "./ConditionOptions";
import { ConditionSelected } from "./ConditionSelected";

View file

@ -1,48 +1,46 @@
/* eslint-disable @typescript-eslint/member-ordering */
import { InitialStateResponse } from "../API/InitialStateResponse";
import { LeftOperand } from "./../useLeftOperands";
import { LeftOperand } from "../LeftOperandsProvider";
import { TokenType, UrlEntry, UrlToken } from "./../ValueProvider/UrlToken";
import { Condition } from "./Condition";
import { ConditionItem, ConditionOptions } from "./ConditionOptions";
import { ConditionSelected } from "./ConditionSelected";
import { ConditionValue, ItemOption } from "./ConditionValue";
class ExpressionValue {
constructor(
public value: string,
public label: string,
public type: string
public type: string,
) {}
public static fromLeftOperand(leftOperand: LeftOperand) {
return new ExpressionValue(
leftOperand.slug,
leftOperand.label,
leftOperand.type
)
leftOperand.type,
);
}
public static fromUrlToken(token: UrlToken) {
return new ExpressionValue(
token.name,
token.name,
token.name
)
return new ExpressionValue(token.name, token.name, token.name);
}
public static forAttribute(attributeName: string, response: InitialStateResponse) {
public static forAttribute(
attributeName: string,
response: InitialStateResponse,
) {
const attribute = response.attributeByName(attributeName);
return new ExpressionValue(
attributeName,
attribute.label,
attribute.inputType,
)
);
}
public static emptyStatic() {
return new ExpressionValue("", "", TokenType.STATIC)
return new ExpressionValue("", "", TokenType.STATIC);
}
}
@ -126,10 +124,10 @@ export class FilterElement {
public asUrlEntry(): UrlEntry {
if (this.isAttribute()) {
return UrlEntry.forAttribute(this.condition.selected, this.value.value)
return UrlEntry.forAttribute(this.condition.selected, this.value.value);
}
return UrlEntry.forStatic(this.condition.selected, this.value.value)
return UrlEntry.forStatic(this.condition.selected, this.value.value);
}
public static fromValueEntry(valueEntry: any) {
@ -165,5 +163,4 @@ export class FilterElement {
}
}
export type FilterContainer = Array<string | FilterElement | FilterContainer>;

View file

@ -0,0 +1,15 @@
import { AttributeInputTypeEnum } from "@dashboard/graphql";
import { StaticElementName } from "./FilterElement/ConditionOptions";
export interface LeftOperand {
type: AttributeInputTypeEnum | StaticElementName;
label: string;
value: string;
slug: string;
}
export interface LeftOperandsProvider {
operands: LeftOperand[];
setOperands: (operands: LeftOperand[]) => void;
}

View file

@ -1,6 +1,4 @@
// @ts-strict-ignore
import { useApolloClient } from "@apollo/client";
import useDebounce from "@dashboard/hooks/useDebounce";
import {
_ExperimentalFilters,
Box,
@ -9,17 +7,14 @@ import {
} from "@saleor/macaw-ui/next";
import React from "react";
import {
getInitialRightOperatorOptions,
getLeftOperatorOptions,
getRightOperatorOptionsByQuery,
} from "./API/getAPIOptions";
import { useProductFilterAPIProvider } from "./API/ProductFilterAPIProvider";
import { useFilterContainer } from "./useFilterContainer";
import { useLeftOperands } from "./useLeftOperands";
import { useFilterLeftOperandsProvider } from "./useFilterLeftOperands";
import { useUrlValueProvider } from "./ValueProvider/useUrlValueProvider";
const FiltersArea = ({ provider, onConfirm }) => {
const client = useApolloClient();
const apiProvider = useProductFilterAPIProvider();
const leftOperandsProvider = useFilterLeftOperandsProvider();
const {
value,
@ -29,46 +24,8 @@ const FiltersArea = ({ provider, onConfirm }) => {
updateRightOperator,
updateCondition,
updateRightOptions,
updateRightLoadingState,
updateLeftLoadingState,
} = useFilterContainer(provider);
const { operands, setOperands } = useLeftOperands();
const handleLeftOperatorInputValueChange = (event: any) => {
const fetchAPI = async () => {
updateLeftLoadingState(event.path, true);
const options = await getLeftOperatorOptions(client, event.value);
updateLeftLoadingState(event.path, false);
setOperands(prev => [...prev, ...options]);
};
fetchAPI();
};
const handleLeftOperatorInputValueChangeDebounced = useDebounce(
handleLeftOperatorInputValueChange,
500,
);
const handleRightOperatorInputValueChange = (event: any) => {
const fetchAPI = async () => {
updateRightLoadingState(event.path.split(".")[0], true);
const options = await getRightOperatorOptionsByQuery(
client,
event.path.split(".")[0],
value,
event.value,
);
updateRightLoadingState(event.path.split(".")[0], false);
updateRightOptions(event.path.split(".")[0], options);
};
fetchAPI();
};
const handleRightOperatorInputValueChangeDebounced = useDebounce(
handleRightOperatorInputValueChange,
500,
);
updateLeftOptions,
} = useFilterContainer(provider, apiProvider, leftOperandsProvider);
const handleStateChange = async (event: FilterEvent["detail"]) => {
if (event.type === "row.add") {
@ -88,24 +45,19 @@ const FiltersArea = ({ provider, onConfirm }) => {
}
if (event.type === "rightOperator.onChange") {
// @ts-expect-error slug in missing in MacawUI
updateRightOperator(event.path.split(".")[0], event.value);
}
if (event.type === "rightOperator.onFocus") {
const path = event.path.split(".")[0];
updateRightLoadingState(path, true);
const options = await getInitialRightOperatorOptions(client, path, value);
updateRightOptions(path, options);
updateRightLoadingState(path, false);
updateRightOptions(event.path.split(".")[0], "");
}
if (event.type === "rightOperator.onInputValueChange") {
handleRightOperatorInputValueChangeDebounced(event);
updateRightOptions(event.path.split(".")[0], event.value);
}
if (event.type === "leftOperator.onInputValueChange") {
handleLeftOperatorInputValueChangeDebounced(event);
updateLeftOptions(event.path.split(".")[0], event.value);
}
};
@ -114,7 +66,7 @@ const FiltersArea = ({ provider, onConfirm }) => {
return (
<Box>
<_ExperimentalFilters
leftOptions={operands}
leftOptions={leftOperandsProvider.operands}
// @ts-expect-error
value={value}
onChange={handleStateChange}

View file

@ -1,38 +1,72 @@
import useDebounce from "@dashboard/hooks/useDebounce";
import { FilterAPIProvider } from "./API/FilterAPIProvider";
import { FilterContainer } from "./FilterElement";
import { ConditionValue, ItemOption } from "./FilterElement/ConditionValue";
import { LeftOperandsProvider } from "./LeftOperandsProvider";
import { useContainerState } from "./useContainerState";
export const useFilterContainer = (initialValue: FilterContainer) => {
const { value, updateAt, removeAt, createEmpty } = useContainerState(initialValue)
export const useFilterContainer = (
initialValue: FilterContainer,
apiProvider: FilterAPIProvider,
leftOperandsProvider: LeftOperandsProvider,
) => {
const { value, updateAt, removeAt, createEmpty } =
useContainerState(initialValue);
const addEmpty = () => {
createEmpty()
createEmpty();
};
const updateLeftOperator = (position: string, leftOperator: any) => {
updateAt(position, (el) => el.updateLeftOperator(leftOperator))
updateAt(position, el => el.updateLeftOperator(leftOperator));
};
const updateLeftLoadingState = (position: string, loading: boolean) => {
updateAt(position, (el) => el.updateLeftLoadingState(loading))
updateAt(position, el => el.updateLeftLoadingState(loading));
};
const updateRightOperator = (position: string, rightOperator: ConditionValue) => {
updateAt(position, (el) => el.updateRightOperator(rightOperator))
const updateRightOperator = (
position: string,
rightOperator: ConditionValue,
) => {
updateAt(position, el => el.updateRightOperator(rightOperator));
};
const updateRightOptions = (position: string, options: ItemOption[]) => {
updateAt(position, (el) => el.updateRightOptions(options))
const _updateRightOptions = (position: string, options: ItemOption[]) => {
updateAt(position, el => el.updateRightOptions(options));
};
const updateRightLoadingState = (position: string, loading: boolean) => {
updateAt(position, (el) => el.updateRightLoadingState(loading))
updateAt(position, el => el.updateRightLoadingState(loading));
};
const updateCondition = (position: string, conditionValue: any) => {
updateAt(position, (el) => el.updateCondition(conditionValue))
updateAt(position, el => el.updateCondition(conditionValue));
};
const _fetchRightOptions = async (position: string, inputValue: string) => {
updateRightLoadingState(position, true);
const options = await apiProvider.fetchRightOptions(
position,
value,
inputValue,
);
updateRightLoadingState(position, false);
_updateRightOptions(position, options);
};
const updateRightOptions = useDebounce(_fetchRightOptions, 500);
const _fetchLeftOptions = async (position: string, inputValue: string) => {
updateLeftLoadingState(position, true);
const options = await apiProvider.fetchLeftOptions(inputValue);
updateLeftLoadingState(position, false);
leftOperandsProvider.setOperands(options);
};
const updateLeftOptions = useDebounce(_fetchLeftOptions, 500);
return {
value,
addEmpty,
@ -41,7 +75,6 @@ export const useFilterContainer = (initialValue: FilterContainer) => {
updateRightOperator,
updateCondition,
updateRightOptions,
updateRightLoadingState,
updateLeftLoadingState,
updateLeftOptions,
};
};

View file

@ -1,14 +1,6 @@
import { AttributeInputTypeEnum } from "@dashboard/graphql";
import { useState } from "react";
import { StaticElementName } from "./FilterElement/ConditionOptions";
export interface LeftOperand {
type: AttributeInputTypeEnum | StaticElementName;
label: string;
value: string;
slug: string;
}
import { LeftOperand, LeftOperandsProvider } from "./LeftOperandsProvider";
const STATIC_OPTIONS: LeftOperand[] = [
{ value: "price", label: "Price", type: "price", slug: "price" },
@ -22,11 +14,12 @@ const STATIC_OPTIONS: LeftOperand[] = [
{ value: "channel", label: "Channel", type: "channel", slug: "channel" },
];
export const useLeftOperands = () => {
export const useFilterLeftOperandsProvider = (): LeftOperandsProvider => {
const [operands, setOperands] = useState<LeftOperand[]>(STATIC_OPTIONS);
return {
operands,
setOperands,
setOperands: (options: LeftOperand[]) =>
setOperands(prev => [...prev, ...options]),
};
};