diff --git a/src/attributes/components/AttributeListPage/AttributeListPage.tsx b/src/attributes/components/AttributeListPage/AttributeListPage.tsx index e9482704a..4a9e71877 100644 --- a/src/attributes/components/AttributeListPage/AttributeListPage.tsx +++ b/src/attributes/components/AttributeListPage/AttributeListPage.tsx @@ -4,19 +4,37 @@ import Card from "@material-ui/core/Card"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; +import SearchBar from "@saleor/components/SearchBar"; import { sectionNames } from "@saleor/intl"; import Container from "../../../components/Container"; import PageHeader from "../../../components/PageHeader"; -import { ListActions, PageListProps } from "../../../types"; +import { + ListActions, + PageListProps, + SearchPageProps, + TabPageProps +} from "../../../types"; import { AttributeList_attributes_edges_node } from "../../types/AttributeList"; import AttributeList from "../AttributeList/AttributeList"; -export interface AttributeListPageProps extends PageListProps, ListActions { +export interface AttributeListPageProps + extends PageListProps, + ListActions, + SearchPageProps, + TabPageProps { attributes: AttributeList_attributes_edges_node[]; } const AttributeListPage: React.FC = ({ onAdd, + initialSearch, + onSearchChange, + currentTab, + onAll, + onTabChange, + onTabDelete, + onTabSave, + tabs, ...listProps }) => { const intl = useIntl(); @@ -32,6 +50,19 @@ const AttributeListPage: React.FC = ({ + diff --git a/src/attributes/queries.ts b/src/attributes/queries.ts index 9b91f4723..66f82d08d 100644 --- a/src/attributes/queries.ts +++ b/src/attributes/queries.ts @@ -52,7 +52,7 @@ const attributeList = gql` ${attributeFragment} ${pageInfoFragment} query AttributeList( - $query: String + $filter: AttributeFilterInput $inCategory: ID $inCollection: ID $before: String @@ -61,7 +61,7 @@ const attributeList = gql` $last: Int ) { attributes( - query: $query + filter: $filter inCategory: $inCategory inCollection: $inCollection before: $before diff --git a/src/attributes/types/AttributeList.ts b/src/attributes/types/AttributeList.ts index 938c0098d..d5f296707 100644 --- a/src/attributes/types/AttributeList.ts +++ b/src/attributes/types/AttributeList.ts @@ -2,6 +2,8 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. +import { AttributeFilterInput } from "./../../types/globalTypes"; + // ==================================================== // GraphQL query operation: AttributeList // ==================================================== @@ -40,7 +42,7 @@ export interface AttributeList { } export interface AttributeListVariables { - query?: string | null; + filter?: AttributeFilterInput | null; inCategory?: string | null; inCollection?: string | null; before?: string | null; diff --git a/src/attributes/urls.ts b/src/attributes/urls.ts index ccd6c28be..80bef3768 100644 --- a/src/attributes/urls.ts +++ b/src/attributes/urls.ts @@ -1,12 +1,26 @@ import { stringify as stringifyQs } from "qs"; import urlJoin from "url-join"; -import { BulkAction, Dialog, Pagination, SingleAction } from "../types"; +import { + ActiveTab, + BulkAction, + Dialog, + Filters, + Pagination, + SingleAction, + TabActionDialog +} from "../types"; export const attributeSection = "/attributes/"; -export type AttributeListUrlDialog = "remove"; -export type AttributeListUrlQueryParams = BulkAction & +export enum AttributeListUrlFiltersEnum { + query = "query" +} +export type AttributeListUrlFilters = Filters; +export type AttributeListUrlDialog = "remove" | TabActionDialog; +export type AttributeListUrlQueryParams = ActiveTab & + AttributeListUrlFilters & + BulkAction & Dialog & Pagination; export const attributeListPath = attributeSection; diff --git a/src/attributes/views/AttributeList/AttributeList.tsx b/src/attributes/views/AttributeList/AttributeList.tsx index 262d4e727..6fc565255 100644 --- a/src/attributes/views/AttributeList/AttributeList.tsx +++ b/src/attributes/views/AttributeList/AttributeList.tsx @@ -3,6 +3,18 @@ import DeleteIcon from "@material-ui/icons/Delete"; import React from "react"; import { useIntl } from "react-intl"; +import { + areFiltersApplied, + deleteFilterTab, + getActiveFilters, + getFilterTabs, + saveFilterTab, + getFilterVariables +} from "@saleor/attributes/views/AttributeList/filters"; +import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; +import SaveFilterTabDialog, { + SaveFilterTabDialogFormData +} from "@saleor/components/SaveFilterTabDialog"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import usePaginator, { @@ -20,6 +32,7 @@ import { attributeAddUrl, attributeListUrl, AttributeListUrlDialog, + AttributeListUrlFilters, AttributeListUrlQueryParams, attributeUrl } from "../../urls"; @@ -37,6 +50,15 @@ const AttributeList: React.FC = ({ params }) => { ); const intl = useIntl(); + const tabs = getFilterTabs(); + + const currentTab = + params.activeTab === undefined + ? areFiltersApplied(params) + ? tabs.length + 1 + : 0 + : parseInt(params.activeTab, 0); + const closeModal = () => navigate( attributeListUrl({ @@ -56,8 +78,46 @@ const AttributeList: React.FC = ({ params }) => { }) ); + const changeFilterField = (filter: AttributeListUrlFilters) => { + reset(); + navigate( + attributeListUrl({ + ...getActiveFilters(params), + ...filter, + activeTab: undefined + }) + ); + }; + + const handleTabChange = (tab: number) => { + reset(); + navigate( + attributeListUrl({ + activeTab: tab.toString(), + ...getFilterTabs()[tab - 1].data + }) + ); + }; + + const handleTabDelete = () => { + deleteFilterTab(currentTab); + reset(); + navigate(attributeListUrl()); + }; + + const handleTabSave = (data: SaveFilterTabDialogFormData) => { + saveFilterTab(data.name, getActiveFilters(params)); + handleTabChange(tabs.length + 1); + }; + const paginationState = createPaginationState(PAGINATE_BY, params); - const queryVariables = React.useMemo(() => paginationState, [params]); + const queryVariables = React.useMemo( + () => ({ + ...paginationState, + filter: getFilterVariables(params) + }), + [params] + ); return ( @@ -99,14 +159,22 @@ const AttributeList: React.FC = ({ params }) => { attributes={maybe(() => data.attributes.edges.map(edge => edge.node) )} + currentTab={currentTab} disabled={loading || attributeBulkDeleteOpts.loading} + initialSearch={params.query || ""} isChecked={isSelected} onAdd={() => navigate(attributeAddUrl())} + onAll={() => navigate(attributeListUrl())} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} onRowClick={id => () => navigate(attributeUrl(id))} + onSearchChange={query => changeFilterField({ query })} + onTabChange={handleTabChange} + onTabDelete={() => openModal("delete-search")} + onTabSave={() => openModal("save-search")} pageInfo={pageInfo} selected={listElements.length} + tabs={tabs.map(tab => tab.name)} toggle={toggle} toggleAll={toggleAll} toolbar={ @@ -130,6 +198,19 @@ const AttributeList: React.FC = ({ params }) => { onClose={closeModal} quantity={maybe(() => params.ids.length)} /> + + tabs[currentTab - 1].name, "...")} + /> ); }} diff --git a/src/attributes/views/AttributeList/filters.ts b/src/attributes/views/AttributeList/filters.ts new file mode 100644 index 000000000..51db99dc9 --- /dev/null +++ b/src/attributes/views/AttributeList/filters.ts @@ -0,0 +1,31 @@ +import { AttributeFilterInput } from "@saleor/types/globalTypes"; +import { + createFilterTabUtils, + createFilterUtils +} from "../../../utils/filters"; +import { + AttributeListUrlFilters, + AttributeListUrlFiltersEnum, + AttributeListUrlQueryParams +} from "../../urls"; + +export const PRODUCT_FILTERS_KEY = "productFilters"; + +export function getFilterVariables( + params: AttributeListUrlFilters + ): AttributeFilterInput { + return { + search: params.query + }; + } + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(PRODUCT_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + AttributeListUrlQueryParams, + AttributeListUrlFilters +>(AttributeListUrlFiltersEnum); diff --git a/src/storybook/stories/attributes/AttributeListPage.tsx b/src/storybook/stories/attributes/AttributeListPage.tsx index e757661f3..c685a4313 100644 --- a/src/storybook/stories/attributes/AttributeListPage.tsx +++ b/src/storybook/stories/attributes/AttributeListPage.tsx @@ -5,12 +5,19 @@ import AttributeListPage, { AttributeListPageProps } from "@saleor/attributes/components/AttributeListPage"; import { attributes } from "@saleor/attributes/fixtures"; -import { listActionsProps, pageListProps } from "@saleor/fixtures"; +import { + listActionsProps, + pageListProps, + searchPageProps, + tabPageProps +} from "@saleor/fixtures"; import Decorator from "../../Decorator"; const props: AttributeListPageProps = { ...pageListProps.default, ...listActionsProps, + ...tabPageProps, + ...searchPageProps, attributes }; diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index 9d49dd07d..e5aacd610 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -168,6 +168,7 @@ export enum PermissionEnum { MANAGE_PAGES = "MANAGE_PAGES", MANAGE_PLUGINS = "MANAGE_PLUGINS", MANAGE_PRODUCTS = "MANAGE_PRODUCTS", + MANAGE_SERVICE_ACCOUNTS = "MANAGE_SERVICE_ACCOUNTS", MANAGE_SETTINGS = "MANAGE_SETTINGS", MANAGE_SHIPPING = "MANAGE_SHIPPING", MANAGE_STAFF = "MANAGE_STAFF", @@ -264,6 +265,17 @@ export interface AttributeCreateInput { availableInGrid?: boolean | null; } +export interface AttributeFilterInput { + valueRequired?: boolean | null; + isVariantOnly?: boolean | null; + visibleInStorefront?: boolean | null; + filterableInStorefront?: boolean | null; + filterableInDashboard?: boolean | null; + availableInGrid?: boolean | null; + search?: string | null; + ids?: (string | null)[] | null; +} + export interface AttributeInput { slug: string; value: string; @@ -431,6 +443,7 @@ export interface OrderFilterInput { status?: (OrderStatusFilter | null)[] | null; customer?: string | null; created?: DateRangeInput | null; + search?: string | null; } export interface OrderLineCreateInput { @@ -577,6 +590,7 @@ export interface StaffCreateInput { note?: string | null; permissions?: (PermissionEnum | null)[] | null; sendPasswordEmail?: boolean | null; + redirectUrl?: string | null; } export interface StaffInput { @@ -605,6 +619,7 @@ export interface UserCreateInput { isActive?: boolean | null; note?: string | null; sendPasswordEmail?: boolean | null; + redirectUrl?: string | null; } export interface VoucherInput {