Add filtering to attributes

This commit is contained in:
dominik-zeglen 2020-01-10 12:34:49 +01:00
parent 04a633bd32
commit d414729f01
6 changed files with 287 additions and 19 deletions

View file

@ -4,25 +4,28 @@ import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import SearchBar from "@saleor/components/SearchBar"; import FilterBar from "@saleor/components/FilterBar";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { AttributeListUrlSortField } from "@saleor/attributes/urls"; import { AttributeListUrlSortField } from "@saleor/attributes/urls";
import { AttributeFilterKeys } from "@saleor/attributes/views/AttributeList/filters";
import { AttributeListFilterOpts } from "@saleor/attributes/types";
import Container from "../../../components/Container"; import Container from "../../../components/Container";
import PageHeader from "../../../components/PageHeader"; import PageHeader from "../../../components/PageHeader";
import { import {
ListActions, ListActions,
PageListProps, PageListProps,
SearchPageProps, FilterPageProps,
TabPageProps, TabPageProps,
SortPage SortPage
} from "../../../types"; } from "../../../types";
import { AttributeList_attributes_edges_node } from "../../types/AttributeList"; import { AttributeList_attributes_edges_node } from "../../types/AttributeList";
import AttributeList from "../AttributeList/AttributeList"; import AttributeList from "../AttributeList/AttributeList";
import { createFilterStructure } from "../../views/AttributeList/filters";
export interface AttributeListPageProps export interface AttributeListPageProps
extends PageListProps, extends PageListProps,
ListActions, ListActions,
SearchPageProps, FilterPageProps<AttributeFilterKeys, AttributeListFilterOpts>,
SortPage<AttributeListUrlSortField>, SortPage<AttributeListUrlSortField>,
TabPageProps { TabPageProps {
attributes: AttributeList_attributes_edges_node[]; attributes: AttributeList_attributes_edges_node[];
@ -30,9 +33,12 @@ export interface AttributeListPageProps
} }
const AttributeListPage: React.FC<AttributeListPageProps> = ({ const AttributeListPage: React.FC<AttributeListPageProps> = ({
currencySymbol,
filterOpts,
initialSearch,
onAdd, onAdd,
onBack, onBack,
initialSearch, onFilterChange,
onSearchChange, onSearchChange,
currentTab, currentTab,
onAll, onAll,
@ -44,6 +50,8 @@ const AttributeListPage: React.FC<AttributeListPageProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const structure = createFilterStructure(intl, filterOpts);
return ( return (
<Container> <Container>
<AppHeader onBack={onBack}> <AppHeader onBack={onBack}>
@ -58,18 +66,21 @@ const AttributeListPage: React.FC<AttributeListPageProps> = ({
</Button> </Button>
</PageHeader> </PageHeader>
<Card> <Card>
<SearchBar <FilterBar
allTabLabel={intl.formatMessage({ allTabLabel={intl.formatMessage({
defaultMessage: "All Attributes", defaultMessage: "All Attributes",
description: "tab name" description: "tab name"
})} })}
currencySymbol={currencySymbol}
currentTab={currentTab} currentTab={currentTab}
filterStructure={structure}
initialSearch={initialSearch} initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage({ searchPlaceholder={intl.formatMessage({
defaultMessage: "Search Attribute" defaultMessage: "Search Attribute"
})} })}
tabs={tabs} tabs={tabs}
onAll={onAll} onAll={onAll}
onFilterChange={onFilterChange}
onSearchChange={onSearchChange} onSearchChange={onSearchChange}
onTabChange={onTabChange} onTabChange={onTabChange}
onTabDelete={onTabDelete} onTabDelete={onTabDelete}

10
src/attributes/types.ts Normal file
View file

@ -0,0 +1,10 @@
import { FilterOpts } from "@saleor/types";
export interface AttributeListFilterOpts {
availableInGrid: FilterOpts<boolean>;
filterableInDashboard: FilterOpts<boolean>;
filterableInStorefront: FilterOpts<boolean>;
isVariantOnly: FilterOpts<boolean>;
valueRequired: FilterOpts<boolean>;
visibleInStorefront: FilterOpts<boolean>;
}

View file

@ -15,6 +15,12 @@ import {
export const attributeSection = "/attributes/"; export const attributeSection = "/attributes/";
export enum AttributeListUrlFiltersEnum { export enum AttributeListUrlFiltersEnum {
availableInGrid = "availableInGrid",
filterableInDashboard = "filterableInDashboard",
filterableInStorefront = "filterableInStorefront",
isVariantOnly = "isVariantOnly",
valueRequired = "valueRequired",
visibleInStorefront = "visibleInStorefront",
query = "query" query = "query"
} }
export type AttributeListUrlFilters = Filters<AttributeListUrlFiltersEnum>; export type AttributeListUrlFilters = Filters<AttributeListUrlFiltersEnum>;

View file

@ -9,7 +9,8 @@ import {
getActiveFilters, getActiveFilters,
getFilterTabs, getFilterTabs,
getFilterVariables, getFilterVariables,
saveFilterTab saveFilterTab,
getFilterOpts
} from "@saleor/attributes/views/AttributeList/filters"; } from "@saleor/attributes/views/AttributeList/filters";
import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
import SaveFilterTabDialog, { import SaveFilterTabDialog, {
@ -24,6 +25,7 @@ import usePaginator, {
import { getSortParams } from "@saleor/utils/sort"; import { getSortParams } from "@saleor/utils/sort";
import createSortHandler from "@saleor/utils/handlers/sortHandler"; import createSortHandler from "@saleor/utils/handlers/sortHandler";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
import { PAGINATE_BY } from "../../../config"; import { PAGINATE_BY } from "../../../config";
import useBulkActions from "../../../hooks/useBulkActions"; import useBulkActions from "../../../hooks/useBulkActions";
import { maybe } from "../../../misc"; import { maybe } from "../../../misc";
@ -35,12 +37,12 @@ import { AttributeBulkDelete } from "../../types/AttributeBulkDelete";
import { import {
attributeAddUrl, attributeAddUrl,
attributeListUrl, attributeListUrl,
AttributeListUrlFilters,
AttributeListUrlQueryParams, AttributeListUrlQueryParams,
attributeUrl, attributeUrl,
AttributeListUrlDialog AttributeListUrlDialog
} from "../../urls"; } from "../../urls";
import { getSortQueryVariables } from "./sort"; import { getSortQueryVariables } from "./sort";
import { getFilterQueryParam } from "./filters";
interface AttributeListProps { interface AttributeListProps {
params: AttributeListUrlQueryParams; params: AttributeListUrlQueryParams;
@ -82,16 +84,17 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
AttributeListUrlQueryParams AttributeListUrlQueryParams
>(navigate, attributeListUrl, params); >(navigate, attributeListUrl, params);
const changeFilterField = (filter: AttributeListUrlFilters) => { const [
reset(); changeFilters,
navigate( resetFilters,
attributeListUrl({ handleSearchChange
...getActiveFilters(params), ] = createFilterHandlers({
...filter, cleanupFn: reset,
activeTab: undefined createUrl: attributeListUrl,
}) getFilterQueryParam,
); navigate,
}; params
});
const handleTabChange = (tab: number) => { const handleTabChange = (tab: number) => {
reset(); reset();
@ -146,15 +149,17 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
)} )}
currentTab={currentTab} currentTab={currentTab}
disabled={loading || attributeBulkDeleteOpts.loading} disabled={loading || attributeBulkDeleteOpts.loading}
filterOpts={getFilterOpts(params)}
initialSearch={params.query || ""} initialSearch={params.query || ""}
isChecked={isSelected} isChecked={isSelected}
onAdd={() => navigate(attributeAddUrl())} onAdd={() => navigate(attributeAddUrl())}
onAll={() => navigate(attributeListUrl())} onAll={resetFilters}
onBack={() => navigate(configurationMenuUrl)} onBack={() => navigate(configurationMenuUrl)}
onFilterChange={changeFilters}
onNextPage={loadNextPage} onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage} onPreviousPage={loadPreviousPage}
onRowClick={id => () => navigate(attributeUrl(id))} onRowClick={id => () => navigate(attributeUrl(id))}
onSearchChange={query => changeFilterField({ query })} onSearchChange={handleSearchChange}
onSort={handleSort} onSort={handleSort}
onTabChange={handleTabChange} onTabChange={handleTabChange}
onTabDelete={() => openModal("delete-search")} onTabDelete={() => openModal("delete-search")}

View file

@ -1,4 +1,10 @@
import { IntlShape } from "react-intl";
import { AttributeFilterInput } from "@saleor/types/globalTypes"; import { AttributeFilterInput } from "@saleor/types/globalTypes";
import { maybe, parseBoolean } from "@saleor/misc";
import { createBooleanField } from "@saleor/utils/filters/fields";
import { commonMessages } from "@saleor/intl";
import { IFilter, IFilterElement } from "@saleor/components/Filter";
import { import {
createFilterTabUtils, createFilterTabUtils,
createFilterUtils createFilterUtils
@ -8,17 +14,217 @@ import {
AttributeListUrlFiltersEnum, AttributeListUrlFiltersEnum,
AttributeListUrlQueryParams AttributeListUrlQueryParams
} from "../../urls"; } from "../../urls";
import { AttributeListFilterOpts } from "../../types";
import messages from "./messages";
export const PRODUCT_FILTERS_KEY = "productFilters"; export const PRODUCT_FILTERS_KEY = "productFilters";
export enum AttributeFilterKeys {
availableInGrid = "availableInGrud",
filterableInDashboard = "filterableInDashboard",
filterableInStorefront = "filterableInStorefront",
isVariantOnly = "isVariantOnly",
valueRequired = "valueRequired",
visibleInStorefront = "visibleInStorefront"
}
export function getFilterOpts(
params: AttributeListUrlFilters
): AttributeListFilterOpts {
return {
availableInGrid: {
active: params.availableInGrid !== undefined,
value: maybe(() => parseBoolean(params.availableInGrid, true))
},
filterableInDashboard: {
active: params.filterableInDashboard !== undefined,
value: maybe(() => parseBoolean(params.filterableInDashboard, true))
},
filterableInStorefront: {
active: params.filterableInStorefront !== undefined,
value: maybe(() => parseBoolean(params.filterableInStorefront, true))
},
isVariantOnly: {
active: params.isVariantOnly !== undefined,
value: maybe(() => parseBoolean(params.isVariantOnly, true))
},
valueRequired: {
active: params.valueRequired !== undefined,
value: maybe(() => parseBoolean(params.valueRequired, true))
},
visibleInStorefront: {
active: params.visibleInStorefront !== undefined,
value: maybe(() => parseBoolean(params.visibleInStorefront, true))
}
};
}
export function createFilterStructure(
intl: IntlShape,
opts: AttributeListFilterOpts
): IFilter<AttributeFilterKeys> {
return [
{
...createBooleanField(
AttributeFilterKeys.availableInGrid,
intl.formatMessage(messages.availableInGrid),
opts.availableInGrid.value,
{
negative: intl.formatMessage(commonMessages.no),
positive: intl.formatMessage(commonMessages.yes)
}
),
active: opts.availableInGrid.active
},
{
...createBooleanField(
AttributeFilterKeys.filterableInDashboard,
intl.formatMessage(messages.filterableInDashboard),
opts.filterableInDashboard.value,
{
negative: intl.formatMessage(commonMessages.no),
positive: intl.formatMessage(commonMessages.yes)
}
),
active: opts.filterableInDashboard.active
},
{
...createBooleanField(
AttributeFilterKeys.filterableInStorefront,
intl.formatMessage(messages.filterableInStorefront),
opts.filterableInStorefront.value,
{
negative: intl.formatMessage(commonMessages.no),
positive: intl.formatMessage(commonMessages.yes)
}
),
active: opts.filterableInStorefront.active
},
{
...createBooleanField(
AttributeFilterKeys.isVariantOnly,
intl.formatMessage(messages.isVariantOnly),
opts.isVariantOnly.value,
{
negative: intl.formatMessage(commonMessages.no),
positive: intl.formatMessage(commonMessages.yes)
}
),
active: opts.isVariantOnly.active
},
{
...createBooleanField(
AttributeFilterKeys.valueRequired,
intl.formatMessage(messages.valueRequired),
opts.valueRequired.value,
{
negative: intl.formatMessage(commonMessages.no),
positive: intl.formatMessage(commonMessages.yes)
}
),
active: opts.valueRequired.active
},
{
...createBooleanField(
AttributeFilterKeys.visibleInStorefront,
intl.formatMessage(messages.visibleInStorefront),
opts.visibleInStorefront.value,
{
negative: intl.formatMessage(commonMessages.no),
positive: intl.formatMessage(commonMessages.yes)
}
),
active: opts.visibleInStorefront.active
}
];
}
export function getFilterVariables( export function getFilterVariables(
params: AttributeListUrlFilters params: AttributeListUrlFilters
): AttributeFilterInput { ): AttributeFilterInput {
return { return {
availableInGrid:
params.availableInGrid !== undefined
? parseBoolean(params.availableInGrid, false)
: undefined,
search: params.query search: params.query
}; };
} }
export function getFilterQueryParam(
filter: IFilterElement<AttributeFilterKeys>
): AttributeListUrlFilters {
const { active, name, value } = filter;
switch (name) {
case AttributeFilterKeys.availableInGrid:
if (!active) {
return {
availableInGrid: undefined
};
}
return {
availableInGrid: value[0]
};
case AttributeFilterKeys.filterableInDashboard:
if (!active) {
return {
filterableInDashboard: undefined
};
}
return {
filterableInDashboard: value[0]
};
case AttributeFilterKeys.filterableInStorefront:
if (!active) {
return {
filterableInStorefront: undefined
};
}
return {
filterableInStorefront: value[0]
};
case AttributeFilterKeys.isVariantOnly:
if (!active) {
return {
isVariantOnly: undefined
};
}
return {
isVariantOnly: value[0]
};
case AttributeFilterKeys.valueRequired:
if (!active) {
return {
valueRequired: undefined
};
}
return {
valueRequired: value[0]
};
case AttributeFilterKeys.visibleInStorefront:
if (!active) {
return {
visibleInStorefront: undefined
};
}
return {
visibleInStorefront: value[0]
};
}
}
export const { export const {
deleteFilterTab, deleteFilterTab,
getFilterTabs, getFilterTabs,

View file

@ -0,0 +1,30 @@
import { defineMessages } from "react-intl";
const messages = defineMessages({
availableInGrid: {
defaultMessage: "Can be used as column",
description: "attribute can be column in product list table"
},
filterableInDashboard: {
defaultMessage: "Filterable in Dashboard",
description: "use attribute in filtering"
},
filterableInStorefront: {
defaultMessage: "Filterable in Storefront",
description: "use attribute in filtering"
},
isVariantOnly: {
defaultMessage: "Variant Only",
description: "attribute can be used only in variants"
},
valueRequired: {
defaultMessage: "Value Required",
description: "attribute value is required"
},
visibleInStorefront: {
defaultMessage: "Visible on Product Page in Storefront",
description: "attribute"
}
});
export default messages;