Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce filters to discount list page #4594

Merged
merged 16 commits into from
Jan 10, 2024
5 changes: 5 additions & 0 deletions .changeset/khaki-cats-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---

Introduce filters to discount list page
3 changes: 2 additions & 1 deletion src/components/ConditionalFilter/UI/constrains.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Row } from "./types";

export const getItemConstraint = (constraint: Row["constraint"]) => ({
disableRemoveButton: !constraint?.removable,
// eslint-disable-next-line @typescript-eslint/key-spacing, @typescript-eslint/no-unnecessary-boolean-literal-compare
poulch marked this conversation as resolved.
Show resolved Hide resolved
disableRemoveButton: constraint?.removable === false,
disableLeftOperator: constraint?.disabled?.includes("left") ?? false,
disableCondition: constraint?.disabled?.includes("condition") ?? false,
disableRightOperator: constraint?.disabled?.includes("right") ?? false,
Expand Down
16 changes: 16 additions & 0 deletions src/components/ConditionalFilter/queryVariables.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
AttributeInput,
DateTimeFilterInput,
DecimalFilterInput,
GlobalIdFilterInput,
ProductWhereInput,
PromotionWhereInput,
} from "@dashboard/graphql";

import { FilterContainer } from "./FilterElement";
Expand Down Expand Up @@ -165,3 +167,17 @@ export const createProductQueryVariables = (
return p;
}, {} as ProductWhereInput);
};

export const creatDiscountsQueryVariables = (
value: FilterContainer,
): PromotionWhereInput => {
return value.reduce((p, c) => {
if (typeof c === "string" || Array.isArray(c)) return p;

p[c.value.value as "endDate" | "startDate"] = createStaticQueryPart(
c.condition.selected,
) as DateTimeFilterInput;

return p;
}, {} as PromotionWhereInput);
};
34 changes: 24 additions & 10 deletions src/discounts/components/DiscountListPage/DiscountListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ExpressionFilters } from "@dashboard/components/AppLayout/ListFilters/components/ExpressionFilters";
import { LegacyFiltersPresetsAlert } from "@dashboard/components/AppLayout/ListFilters/components/LegacyFiltersPresetsAlert";
import SearchInput from "@dashboard/components/AppLayout/ListFilters/components/SearchInput";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { DashboardCard } from "@dashboard/components/Card";
Expand Down Expand Up @@ -102,17 +104,29 @@ const DiscountListPage: React.FC<DiscountListPageProps> = ({
</TopNav>

<DashboardCard>
<Box __width="320px" marginLeft={4} marginBottom={2}>
{/* TODO: remove when new fileters will be implemented */}
<SearchInput
initialSearch={initialSearch}
placeholder={intl.formatMessage({
id: "+bhokL",
defaultMessage: "Search discounts...",
})}
onSearchChange={onSearchChange}
/>
<LegacyFiltersPresetsAlert />
<Box
display="grid"
__gridTemplateColumns="auto 1fr"
gap={4}
paddingBottom={2}
paddingX={6}
>
<Box display="flex" alignItems="center" gap={4}>
<ExpressionFilters data-test-id="filters-button" />
<Box __width="320px">
<SearchInput
initialSearch={initialSearch}
placeholder={intl.formatMessage({
id: "+bhokL",
defaultMessage: "Search discounts...",
})}
onSearchChange={onSearchChange}
/>
</Box>
</Box>
</Box>

<DiscountListDatagrid {...listProps} onRowClick={handleRowClick} />
</DashboardCard>
</ListPageLayout>
Expand Down
29 changes: 29 additions & 0 deletions src/discounts/filters/provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ConditionalFilterContext } from "@dashboard/components/ConditionalFilter/context/context";
poulch marked this conversation as resolved.
Show resolved Hide resolved
import { useContainerState } from "@dashboard/components/ConditionalFilter/useContainerState";
import React, { FC } from "react";

import { useDiscountFilterAPIProvider } from "./useDiscountFilterAPIProvider";
import { useFilterLeftOperandsProvider } from "./useFilterLeftOperandsProvider";
import { useUrlValueProvider } from "./useUrlValueProvider";

export const ConditionalDiscountFilterProvider: FC<{
locationSearch: string;
}> = ({ children, locationSearch }) => {
const apiProvider = useDiscountFilterAPIProvider();
const valueProvider = useUrlValueProvider(locationSearch);
const leftOperandsProvider = useFilterLeftOperandsProvider();
const containerState = useContainerState(valueProvider);

return (
<ConditionalFilterContext.Provider
value={{
apiProvider,
valueProvider,
leftOperandsProvider,
containerState,
}}
>
{children}
</ConditionalFilterContext.Provider>
);
};
16 changes: 16 additions & 0 deletions src/discounts/filters/useDiscountFilterAPIProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FilterAPIProvider } from "@dashboard/components/ConditionalFilter/API/FilterAPIProvider";
poulch marked this conversation as resolved.
Show resolved Hide resolved

export const useDiscountFilterAPIProvider = (): FilterAPIProvider => {
const fetchRightOptions = async () => {
return [];
};

const fetchLeftOptions = async () => {
return [];
};

return {
fetchRightOptions,
fetchLeftOptions,
};
};
29 changes: 29 additions & 0 deletions src/discounts/filters/useFilterLeftOperandsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { LeftOperand } from "@dashboard/components/ConditionalFilter/LeftOperandsProvider";
import { AttributeInputTypeEnum } from "@dashboard/graphql";
import unionBy from "lodash/unionBy";
import { useState } from "react";

export const STATIC_OPTIONS: LeftOperand[] = [
{
value: "startDate",
label: "Start date",
type: AttributeInputTypeEnum.DATE_TIME,
slug: "startDate",
},
{
value: "endDate",
label: "End date",
type: AttributeInputTypeEnum.DATE_TIME,
slug: "endDate",
},
];

export const useFilterLeftOperandsProvider = () => {
poulch marked this conversation as resolved.
Show resolved Hide resolved
const [operands, setOperands] = useState<LeftOperand[]>(STATIC_OPTIONS);

return {
operands,
setOperands: (options: LeftOperand[]) =>
setOperands(prev => unionBy([...prev, ...options], "value")),
};
};
110 changes: 110 additions & 0 deletions src/discounts/filters/useUrlValueProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { InitialStateResponse } from "@dashboard/components/ConditionalFilter/API/InitialStateResponse";
import {
FilterContainer,
FilterElement,
} from "@dashboard/components/ConditionalFilter/FilterElement";
import { FilterValueProvider } from "@dashboard/components/ConditionalFilter/FilterValueProvider";
import { TokenArray } from "@dashboard/components/ConditionalFilter/ValueProvider/TokenArray";
import { UrlEntry } from "@dashboard/components/ConditionalFilter/ValueProvider/UrlToken";
import { stringify } from "qs";
import { useEffect, useState } from "react";
import useRouter from "use-react-router";

type Structure = Array<string | UrlEntry | Structure>;

const prepareStructure = (filterValue: FilterContainer): Structure =>
filterValue.map(f => {
if (typeof f === "string") {
return f;
}

if (Array.isArray(f)) {
return prepareStructure(f);
}

return f.asUrlEntry();
});

export const useUrlValueProvider = (
locationSearch: string,
): FilterValueProvider => {
const router = useRouter();
const params = new URLSearchParams(locationSearch);
const [value, setValue] = useState<FilterContainer>([]);

const activeTab = params.get("activeTab");
const query = params.get("query");
const before = params.get("before");
const after = params.get("after");
params.delete("asc");
params.delete("sort");
params.delete("activeTab");
params.delete("query");
params.delete("before");
params.delete("after");

const tokenizedUrl = new TokenArray(params.toString());

useEffect(() => {
const emptyInitialState = InitialStateResponse.empty();
emptyInitialState.attribute = {
startDate: {
choices: [],
inputType: "DATE_TIME",
label: "Start date",
slug: "start-date",
value: "start-date",
},
endDate: {
choices: [],
inputType: "DATE_TIME",
label: "End date",
slug: "end-date",
value: "end-date",
},
};

setValue(tokenizedUrl.asFilterValuesFromResponse(emptyInitialState));
}, [locationSearch]);
poulch marked this conversation as resolved.
Show resolved Hide resolved

const persist = (filterValue: FilterContainer) => {
router.history.replace({
pathname: router.location.pathname,
search: stringify({
...prepareStructure(filterValue),
...{ activeTab: activeTab || undefined },
...{ query: query || undefined },
...{ before: before || undefined },
...{ after: after || undefined },
}),
});
setValue(filterValue);
};

const clear = () => {
router.history.replace({
pathname: router.location.pathname,
});
setValue([]);
};

const isPersisted = (element: FilterElement) => {
return value.some(p => FilterElement.isCompatible(p) && p.equals(element));
};

const getTokenByName = (name: string) => {
return tokenizedUrl.asFlatArray().find(token => token.name === name);
};

const count = value.filter(v => typeof v !== "string").length;

return {
value,
loading: false,
persist,
clear,
isPersisted,
getTokenByName,
count,
};
};
7 changes: 6 additions & 1 deletion src/discounts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DiscountListUrlQueryParams,
DiscountListUrlSortField,
} from "./discountsUrls";
import { ConditionalDiscountFilterProvider } from "./filters/provider";
import {
saleAddPath,
saleListPath,
Expand Down Expand Up @@ -45,7 +46,11 @@ const SaleListView: React.FC<RouteComponentProps<{}>> = ({ location }) => {
qs,
DiscountListUrlSortField,
);
return <DiscountList params={params} />;
return (
<ConditionalDiscountFilterProvider locationSearch={location.search}>
<DiscountList params={params} />;
</ConditionalDiscountFilterProvider>
);
}

return <SaleListViewComponent params={params} />;
Expand Down
7 changes: 6 additions & 1 deletion src/discounts/views/DiscountList/DiscountList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useConditionalFilterContext } from "@dashboard/components/ConditionalFilter";
import { creatDiscountsQueryVariables } from "@dashboard/components/ConditionalFilter/queryVariables";
import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog";
import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog";
import { WindowTitle } from "@dashboard/components/WindowTitle";
Expand Down Expand Up @@ -44,11 +46,15 @@ export const DiscountList: React.FC<DiscountListProps> = ({ params }) => {
usePaginationReset(discountListUrl, params, settings.rowNumber);
const paginationState = createPaginationState(settings.rowNumber, params);

const { valueProvider } = useConditionalFilterContext();
const where = creatDiscountsQueryVariables(valueProvider.value);

const queryVariables = React.useMemo(
() => ({
...paginationState,
sort: getSortQueryVariables(params),
where: {
...where,
...(params?.query && {
name: { eq: params.query },
}),
Expand All @@ -68,7 +74,6 @@ export const DiscountList: React.FC<DiscountListProps> = ({ params }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, resetFilters, handleSearchChange] = createFilterHandlers({
createUrl: discountListUrl,
// TODO: implement getFilterQueryParam when new filter will be implemented
getFilterQueryParam: () => 0,
navigate,
params,
Expand Down
Loading