Experimental filters: use context for data providers (#3899)

This commit is contained in:
Krzysztof Żuraw 2023-07-13 09:31:16 +02:00 committed by GitHub
parent b386cf060f
commit fcd64f65eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 129 additions and 62 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---
Experimental filters: use context for data providers

View file

@ -1,4 +1,4 @@
import { ConditionalProductFilters } from "@dashboard/components/ConditionalFilter"; import { ConditionalFilters } from "@dashboard/components/ConditionalFilter";
import { Box, Button, Popover } from "@saleor/macaw-ui/next"; import { Box, Button, Popover } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
@ -10,7 +10,7 @@ export const ExpressionFilters = () => (
<Popover.Content align="start"> <Popover.Content align="start">
<Box __minWidth="200px" __minHeight="100px" paddingX={4} paddingY={3}> <Box __minWidth="200px" __minHeight="100px" paddingX={4} paddingY={3}>
<Popover.Arrow /> <Popover.Arrow />
<ConditionalProductFilters /> <ConditionalFilters />
</Box> </Box>
</Popover.Content> </Popover.Content>
</Popover> </Popover>

View file

@ -1,6 +1,8 @@
import { FilterContainer } from "../FilterElement"; import { FilterContainer } from "../FilterElement";
import { ItemOption } from "../FilterElement/ConditionValue"; import { ItemOption } from "../FilterElement/ConditionValue";
import { LeftOperand } from "../LeftOperandsProvider"; import { LeftOperand } from "../LeftOperandsProvider";
import { FetchingParams } from "../ValueProvider/TokenArray/fetchingParams";
import { InitialStateResponse } from "./InitialStateResponse";
export interface FilterAPIProvider { export interface FilterAPIProvider {
fetchRightOptions: ( fetchRightOptions: (
@ -9,4 +11,8 @@ export interface FilterAPIProvider {
inputValue: string, inputValue: string,
) => Promise<ItemOption[]>; ) => Promise<ItemOption[]>;
fetchLeftOptions: (inputValue: string) => Promise<LeftOperand[]>; fetchLeftOptions: (inputValue: string) => Promise<LeftOperand[]>;
useInitialState: (fetchingParams: FetchingParams) => {
data: InitialStateResponse;
loading: boolean;
};
} }

View file

@ -1,6 +1,7 @@
import { ApolloClient, useApolloClient } from "@apollo/client"; import { ApolloClient, useApolloClient } from "@apollo/client";
import { FilterContainer, FilterElement } from "../FilterElement"; import { FilterContainer, FilterElement } from "../FilterElement";
import { FetchingParams } from "../ValueProvider/TokenArray/fetchingParams";
import { FilterAPIProvider } from "./FilterAPIProvider"; import { FilterAPIProvider } from "./FilterAPIProvider";
import { import {
AttributeChoicesHandler, AttributeChoicesHandler,
@ -11,6 +12,11 @@ import {
Handler, Handler,
ProductTypeHandler, ProductTypeHandler,
} from "./Handler"; } from "./Handler";
import {
createInitialStateFromData,
useDataFromAPI,
} from "./initialState/helpers";
import { InitialStateResponse } from "./InitialStateResponse";
const getFilterElement = ( const getFilterElement = (
value: FilterContainer, value: FilterContainer,
@ -79,8 +85,31 @@ export const useProductFilterAPIProvider = (): FilterAPIProvider => {
return handler.fetch(); return handler.fetch();
}; };
const useInitialState = (fetchingParams: FetchingParams) => {
const { data, loading } = useDataFromAPI({
...fetchingParams,
});
const initialState = createInitialStateFromData(
data,
fetchingParams.channel,
);
return {
data: new InitialStateResponse(
initialState.category,
initialState.attribute,
initialState.channel,
initialState.collection,
initialState.producttype,
),
loading,
};
};
return { return {
fetchRightOptions, fetchRightOptions,
fetchLeftOptions, fetchLeftOptions,
useInitialState,
}; };
}; };

View file

@ -2,7 +2,14 @@ import { FetchingParams } from "../../ValueProvider/TokenArray/fetchingParams";
import { InitialStateResponse } from "../InitialStateResponse"; import { InitialStateResponse } from "../InitialStateResponse";
import { createInitialStateFromData, useDataFromAPI } from "./helpers"; import { createInitialStateFromData, useDataFromAPI } from "./helpers";
export const useInitialAPIState = (props: FetchingParams) => { export interface InitialStateAPIProvider {
data: InitialStateResponse;
loading: boolean;
}
export const useInitialAPIState = (
props: FetchingParams,
): InitialStateAPIProvider => {
const { data, loading } = useDataFromAPI({ const { data, loading } = useDataFromAPI({
...props, ...props,
}); });

View file

@ -1,24 +1,16 @@
import { Box, Text } from "@saleor/macaw-ui/next"; import { Box, Text } from "@saleor/macaw-ui/next";
import React from "react"; import React, { FC } from "react";
import { FilterAPIProvider } from "./API/FilterAPIProvider"; import { useConditionalFilterContext } from "./context";
import { FilterContainer } from "./FilterElement"; import { FilterContainer } from "./FilterElement";
import { FiltersArea } from "./FiltersArea"; import { FiltersArea } from "./FiltersArea";
import { FilterValueProvider } from "./FilterValueProvider";
import { LeftOperandsProvider } from "./LeftOperandsProvider";
interface ConditionalFiltersProps { export const ConditionalFilters: FC = () => {
valueProvider: FilterValueProvider const { valueProvider } = useConditionalFilterContext();
apiProvider: FilterAPIProvider
leftOperandsProvider: LeftOperandsProvider
onConfirm: (value: FilterContainer) => void
}
export const ConditionalFilters = ({ valueProvider, apiProvider, leftOperandsProvider, onConfirm }: ConditionalFiltersProps) => {
const handleConfirm = (value: FilterContainer) => { const handleConfirm = (value: FilterContainer) => {
valueProvider.persist(value) valueProvider.persist(value);
onConfirm(value) };
}
return ( return (
<Box> <Box>
@ -26,8 +18,6 @@ export const ConditionalFilters = ({ valueProvider, apiProvider, leftOperandsPro
<Text>Loading...</Text> <Text>Loading...</Text>
) : ( ) : (
<FiltersArea <FiltersArea
apiProvider={apiProvider}
leftOperandsProvider={leftOperandsProvider}
filterValue={valueProvider.value} filterValue={valueProvider.value}
onConfirm={handleConfirm} onConfirm={handleConfirm}
/> />

View file

@ -1,23 +1,18 @@
import { import { _ExperimentalFilters, Box, FilterEvent } from "@saleor/macaw-ui/next";
_ExperimentalFilters,
Box,
FilterEvent,
} from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { FilterAPIProvider } from "./API/FilterAPIProvider"; import { useConditionalFilterContext } from "./context";
import { FilterContainer } from "./FilterElement"; import { FilterContainer } from "./FilterElement";
import { LeftOperandsProvider } from "./LeftOperandsProvider";
import { useFilterContainer } from "./useFilterContainer"; import { useFilterContainer } from "./useFilterContainer";
interface FiltersAreaProps { interface FiltersAreaProps {
filterValue: FilterContainer filterValue: FilterContainer;
apiProvider: FilterAPIProvider onConfirm: (value: FilterContainer) => void;
leftOperandsProvider: LeftOperandsProvider
onConfirm: (value: FilterContainer) => void
} }
export const FiltersArea = ({ filterValue, apiProvider, leftOperandsProvider, onConfirm }: FiltersAreaProps) => { export const FiltersArea = ({ filterValue, onConfirm }: FiltersAreaProps) => {
const { apiProvider, leftOperandsProvider } = useConditionalFilterContext();
const { const {
value, value,
addEmpty, addEmpty,
@ -30,7 +25,7 @@ export const FiltersArea = ({ filterValue, apiProvider, leftOperandsProvider, on
} = useFilterContainer(filterValue, apiProvider, leftOperandsProvider); } = useFilterContainer(filterValue, apiProvider, leftOperandsProvider);
const handleStateChange = async (event: FilterEvent["detail"]) => { const handleStateChange = async (event: FilterEvent["detail"]) => {
if (!event) return if (!event) return;
if (event.type === "row.add") { if (event.type === "row.add") {
addEmpty(); addEmpty();

View file

@ -1,7 +1,7 @@
import { stringify } from "qs"; import { stringify } from "qs";
import useRouter from "use-react-router"; import useRouter from "use-react-router";
import { useInitialAPIState } from "../API/initialState/useInitalAPIState"; import { FilterAPIProvider } from "../API/FilterAPIProvider";
import { FilterContainer } from "../FilterElement"; import { FilterContainer } from "../FilterElement";
import { FilterValueProvider } from "../FilterValueProvider"; import { FilterValueProvider } from "../FilterValueProvider";
import { useTokenArray } from "./TokenArray"; import { useTokenArray } from "./TokenArray";
@ -22,7 +22,9 @@ const prepareStructure = (filterValue: FilterContainer): Structure =>
return f.asUrlEntry(); return f.asUrlEntry();
}); });
export const useUrlValueProvider = (): FilterValueProvider => { export const useUrlValueProvider = (
apiProvider: FilterAPIProvider,
): FilterValueProvider => {
const router = useRouter(); const router = useRouter();
const params = new URLSearchParams(router.location.search); const params = new URLSearchParams(router.location.search);
params.delete("asc"); params.delete("asc");
@ -30,7 +32,7 @@ export const useUrlValueProvider = (): FilterValueProvider => {
const tokenizedUrl = useTokenArray(params.toString()); const tokenizedUrl = useTokenArray(params.toString());
const fetchingParams = tokenizedUrl.getFetchingParams(); const fetchingParams = tokenizedUrl.getFetchingParams();
const { data, loading } = useInitialAPIState(fetchingParams); const { data, loading } = apiProvider.useInitialState(fetchingParams);
const value = loading ? [] : tokenizedUrl.asFilterValuesFromResponse(data); const value = loading ? [] : tokenizedUrl.asFilterValuesFromResponse(data);
const persist = (filterValue: FilterContainer) => { const persist = (filterValue: FilterContainer) => {

View file

@ -0,0 +1,15 @@
import { useContext } from "react";
import { ConditionalFilterContext } from "./context";
export const useConditionalFilterContext = () => {
const context = useContext(ConditionalFilterContext);
if (!context) {
throw new Error(
"Filter context must be used within ConditionalFilterContext.Provider.",
);
}
return context;
};

View file

@ -0,0 +1,11 @@
import { createContext } from "react";
import { FilterAPIProvider } from "../API/FilterAPIProvider";
import { FilterValueProvider } from "../FilterValueProvider";
import { LeftOperandsProvider } from "../LeftOperandsProvider";
export const ConditionalFilterContext = createContext<{
apiProvider: FilterAPIProvider;
valueProvider: FilterValueProvider;
leftOperandsProvider: LeftOperandsProvider;
} | null>(null);

View file

@ -0,0 +1,2 @@
export * from "./consumer";
export * from "./provider";

View file

@ -0,0 +1,24 @@
import React, { FC } from "react";
import { useProductFilterAPIProvider } from "../API/ProductFilterAPIProvider";
import { useFilterLeftOperandsProvider } from "../useFilterLeftOperands";
import { useUrlValueProvider } from "../ValueProvider/useUrlValueProvider";
import { ConditionalFilterContext } from "./context";
export const ConditionalProductFilterProvider: FC = ({ children }) => {
const apiProvider = useProductFilterAPIProvider();
const valueProvider = useUrlValueProvider(apiProvider);
const leftOperandsProvider = useFilterLeftOperandsProvider();
return (
<ConditionalFilterContext.Provider
value={{
apiProvider,
valueProvider,
leftOperandsProvider,
}}
>
{children}
</ConditionalFilterContext.Provider>
);
};

View file

@ -0,0 +1 @@
export * from "./ConditionalFilters";

View file

@ -1,25 +0,0 @@
import React from "react";
import { useProductFilterAPIProvider } from "./API/ProductFilterAPIProvider";
import { ConditionalFilters } from "./ConditionalFilters";
import { FilterContainer } from "./FilterElement";
import { useFilterLeftOperandsProvider } from "./useFilterLeftOperands";
import { useUrlValueProvider } from "./ValueProvider/useUrlValueProvider";
export const ConditionalProductFilters = () => {
const provider = useUrlValueProvider();
const apiProvider = useProductFilterAPIProvider();
const leftOperandsProvider = useFilterLeftOperandsProvider();
// @ts-expect-error
const handleConfirm = (value: FilterContainer) => {
}
return <ConditionalFilters
valueProvider={provider}
apiProvider={apiProvider}
leftOperandsProvider={leftOperandsProvider}
onConfirm={handleConfirm}
/>
}

View file

@ -1,3 +1,4 @@
import { ConditionalProductFilterProvider } from "@dashboard/components/ConditionalFilter/context";
import { sectionNames } from "@dashboard/intl"; import { sectionNames } from "@dashboard/intl";
import { asSortParams } from "@dashboard/utils/sort"; import { asSortParams } from "@dashboard/utils/sort";
import { getArrayQueryParam } from "@dashboard/utils/urls"; import { getArrayQueryParam } from "@dashboard/utils/urls";
@ -43,7 +44,11 @@ const ProductList: React.FC<RouteComponentProps<any>> = ({ location }) => {
ProductListUrlSortField, ProductListUrlSortField,
); );
return <ProductListComponent params={params} />; return (
<ConditionalProductFilterProvider>
<ProductListComponent params={params} />
</ConditionalProductFilterProvider>
);
}; };
const ProductUpdate: React.FC<RouteComponentProps<any>> = ({ match }) => { const ProductUpdate: React.FC<RouteComponentProps<any>> = ({ match }) => {