diff --git a/src/categories/components/CategoryList/CategoryList.tsx b/src/categories/components/CategoryList/CategoryList.tsx index aff5a0851..dc18b965a 100644 --- a/src/categories/components/CategoryList/CategoryList.tsx +++ b/src/categories/components/CategoryList/CategoryList.tsx @@ -1,5 +1,3 @@ -import Button from "@material-ui/core/Button"; -import Card from "@material-ui/core/Card"; import { createStyles, Theme, @@ -12,9 +10,9 @@ import TableCell from "@material-ui/core/TableCell"; import TableFooter from "@material-ui/core/TableFooter"; import TableRow from "@material-ui/core/TableRow"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { FormattedMessage } from "react-intl"; -import CardTitle from "@saleor/components/CardTitle"; +import { CategoryFragment } from "@saleor/categories/types/CategoryFragment"; import Checkbox from "@saleor/components/Checkbox"; import Skeleton from "@saleor/components/Skeleton"; import TableHead from "@saleor/components/TableHead"; @@ -49,20 +47,8 @@ const styles = (theme: Theme) => } }); -interface CategoryListProps - extends ListProps, - ListActions, - WithStyles { - categories?: Array<{ - id: string; - name: string; - children: { - totalCount: number; - }; - products: { - totalCount: number; - }; - }>; +interface CategoryListProps extends ListProps, ListActions { + categories?: CategoryFragment[]; isRoot: boolean; onAdd?(); } @@ -75,144 +61,119 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })( classes, disabled, settings, - isRoot, pageInfo, isChecked, + isRoot, selected, toggle, toggleAll, toolbar, - onAdd, onNextPage, onPreviousPage, onUpdateListSettings, onRowClick - }: CategoryListProps) => { - const intl = useIntl(); - - return ( - - {!isRoot && ( - - - - } + }: CategoryListProps & WithStyles) => ( + + + + + + + - )} -
- + + + + + + + - - - - - - - - - - - - - - - - - {renderCollection( - categories, - category => { - const isSelected = category ? isChecked(category.id) : false; + settings={settings} + hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false} + onNextPage={onNextPage} + onUpdateListSettings={onUpdateListSettings} + hasPreviousPage={ + pageInfo && !disabled ? pageInfo.hasPreviousPage : false + } + onPreviousPage={onPreviousPage} + /> + + + + {renderCollection( + categories, + category => { + const isSelected = category ? isChecked(category.id) : false; - return ( - - - toggle(category.id)} - /> - - - {category && category.name ? category.name : } - - - {category && - category.children && - category.children.totalCount !== undefined ? ( - category.children.totalCount - ) : ( - - )} - - - {category && - category.products && - category.products.totalCount !== undefined ? ( - category.products.totalCount - ) : ( - - )} - - - ); - }, - () => ( - - - {isRoot ? ( - - ) : ( - - )} - - - ) - )} - -
-
- ); - } + return ( + + + toggle(category.id)} + /> + + + {category && category.name ? category.name : } + + + {category && + category.children && + category.children.totalCount !== undefined ? ( + category.children.totalCount + ) : ( + + )} + + + {category && + category.products && + category.products.totalCount !== undefined ? ( + category.products.totalCount + ) : ( + + )} + + + ); + }, + () => ( + + + {isRoot ? ( + + ) : ( + + )} + + + ) + )} + + + ) ); CategoryList.displayName = "CategoryList"; export default CategoryList; diff --git a/src/categories/components/CategoryListPage/CategoryListPage.tsx b/src/categories/components/CategoryListPage/CategoryListPage.tsx index b552a482e..2e147f1e7 100644 --- a/src/categories/components/CategoryListPage/CategoryListPage.tsx +++ b/src/categories/components/CategoryListPage/CategoryListPage.tsx @@ -1,44 +1,55 @@ import Button from "@material-ui/core/Button"; - +import Card from "@material-ui/core/Card"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; +import { CategoryFragment } from "@saleor/categories/types/CategoryFragment"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; +import SearchBar from "@saleor/components/SearchBar"; import { sectionNames } from "@saleor/intl"; -import { ListActions, PageListProps } from "@saleor/types"; +import { + ListActions, + PageListProps, + SearchPageProps, + TabPageProps +} from "@saleor/types"; import CategoryList from "../CategoryList"; -export interface CategoryTableProps extends PageListProps, ListActions { - categories: Array<{ - id: string; - name: string; - children: { - totalCount: number; - }; - products: { - totalCount: number; - }; - }>; +export interface CategoryTableProps + extends PageListProps, + ListActions, + SearchPageProps, + TabPageProps { + categories: CategoryFragment[]; } export const CategoryListPage: React.StatelessComponent = ({ categories, + currentTab, disabled, - settings, - onAdd, - onNextPage, - onPreviousPage, - onUpdateListSettings, - onRowClick, - pageInfo, + initialSearch, isChecked, + pageInfo, selected, + settings, + tabs, toggle, toggleAll, - toolbar + toolbar, + onAdd, + onAll, + onNextPage, + onPreviousPage, + onRowClick, + onSearchChange, + onTabChange, + onTabDelete, + onTabSave, + onUpdateListSettings }) => { const intl = useIntl(); + return ( @@ -49,23 +60,38 @@ export const CategoryListPage: React.StatelessComponent = ({ /> - + + + + ); }; diff --git a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx index 0e33eef2c..aa3f7d715 100644 --- a/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx +++ b/src/categories/components/CategoryUpdatePage/CategoryUpdatePage.tsx @@ -1,9 +1,12 @@ +import Button from "@material-ui/core/Button"; +import Card from "@material-ui/core/Card"; import { RawDraftContentState } from "draft-js"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import AppHeader from "@saleor/components/AppHeader"; import { CardSpacer } from "@saleor/components/CardSpacer"; +import CardTitle from "@saleor/components/CardTitle"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; import Form from "@saleor/components/Form"; @@ -178,21 +181,40 @@ export const CategoryUpdatePage: React.StatelessComponent< {currentTab === CategoryPageTab.categories && ( - + + + + + } + /> + + )} {currentTab === CategoryPageTab.products && ( ( ); export const categoryDetails = gql` + ${categoryFragment} ${categoryDetailsFragment} query CategoryDetails( $id: ID! @@ -77,14 +86,7 @@ export const categoryDetails = gql` children(first: 20) { edges { node { - id - name - children { - totalCount - } - products { - totalCount - } + ...CategoryFragment } } } diff --git a/src/categories/types/CategoryFragment.ts b/src/categories/types/CategoryFragment.ts new file mode 100644 index 000000000..d01e37e25 --- /dev/null +++ b/src/categories/types/CategoryFragment.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL fragment: CategoryFragment +// ==================================================== + +export interface CategoryFragment_children { + __typename: "CategoryCountableConnection"; + totalCount: number | null; +} + +export interface CategoryFragment_products { + __typename: "ProductCountableConnection"; + totalCount: number | null; +} + +export interface CategoryFragment { + __typename: "Category"; + id: string; + name: string; + children: CategoryFragment_children | null; + products: CategoryFragment_products | null; +} diff --git a/src/categories/types/RootCategories.ts b/src/categories/types/RootCategories.ts index 0c55c71df..2517b129f 100644 --- a/src/categories/types/RootCategories.ts +++ b/src/categories/types/RootCategories.ts @@ -2,6 +2,8 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. +import { CategoryFilterInput } from "./../../types/globalTypes"; + // ==================================================== // GraphQL query operation: RootCategories // ==================================================== @@ -52,4 +54,5 @@ export interface RootCategoriesVariables { after?: string | null; last?: number | null; before?: string | null; + filter?: CategoryFilterInput | null; } diff --git a/src/categories/urls.ts b/src/categories/urls.ts index 6d49a8a85..1c06252e7 100644 --- a/src/categories/urls.ts +++ b/src/categories/urls.ts @@ -1,14 +1,27 @@ import { stringify as stringifyQs } from "qs"; import urlJoin from "url-join"; -import { ActiveTab, BulkAction, Dialog, Pagination } from "../types"; +import { + ActiveTab, + BulkAction, + Dialog, + Filters, + Pagination, + TabActionDialog +} from "../types"; import { CategoryPageTab } from "./components/CategoryUpdatePage"; const categorySectionUrl = "/categories/"; export const categoryListPath = categorySectionUrl; -export type CategoryListUrlDialog = "delete"; -export type CategoryListUrlQueryParams = BulkAction & +export enum CategoryListUrlFiltersEnum { + query = "query" +} +export type CategoryListUrlFilters = Filters; +export type CategoryListUrlDialog = "delete" | TabActionDialog; +export type CategoryListUrlQueryParams = ActiveTab & + BulkAction & + CategoryListUrlFilters & Dialog & Pagination; export const categoryListUrl = (params?: CategoryListUrlQueryParams) => diff --git a/src/categories/views/CategoryList.tsx b/src/categories/views/CategoryList/CategoryList.tsx similarity index 62% rename from src/categories/views/CategoryList.tsx rename to src/categories/views/CategoryList/CategoryList.tsx index 1064d9ccc..be57e3bda 100644 --- a/src/categories/views/CategoryList.tsx +++ b/src/categories/views/CategoryList/CategoryList.tsx @@ -5,6 +5,10 @@ import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import ActionDialog from "@saleor/components/ActionDialog"; +import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; +import SaveFilterTabDialog, { + SaveFilterTabDialogFormData +} from "@saleor/components/SaveFilterTabDialog"; import useBulkActions from "@saleor/hooks/useBulkActions"; import useListSettings from "@saleor/hooks/useListSettings"; import useNavigator from "@saleor/hooks/useNavigator"; @@ -13,16 +17,26 @@ import usePaginator, { } from "@saleor/hooks/usePaginator"; import { getMutationState, maybe } from "@saleor/misc"; import { ListViews } from "@saleor/types"; -import { CategoryListPage } from "../components/CategoryListPage/CategoryListPage"; -import { TypedCategoryBulkDeleteMutation } from "../mutations"; -import { TypedRootCategoriesQuery } from "../queries"; -import { CategoryBulkDelete } from "../types/CategoryBulkDelete"; +import { CategoryListPage } from "../../components/CategoryListPage/CategoryListPage"; +import { TypedCategoryBulkDeleteMutation } from "../../mutations"; +import { TypedRootCategoriesQuery } from "../../queries"; +import { CategoryBulkDelete } from "../../types/CategoryBulkDelete"; import { categoryAddUrl, categoryListUrl, + CategoryListUrlDialog, + CategoryListUrlFilters, CategoryListUrlQueryParams, categoryUrl -} from "../urls"; +} from "../../urls"; +import { + areFiltersApplied, + deleteFilterTab, + getActiveFilters, + getFilterTabs, + getFilterVariables, + saveFilterTab +} from "./filter"; interface CategoryListProps { params: CategoryListUrlQueryParams; @@ -41,9 +55,77 @@ export const CategoryList: React.StatelessComponent = ({ ); const intl = useIntl(); + const tabs = getFilterTabs(); + + const currentTab = + params.activeTab === undefined + ? areFiltersApplied(params) + ? tabs.length + 1 + : 0 + : parseInt(params.activeTab, 0); + + const changeFilterField = (filter: CategoryListUrlFilters) => { + reset(); + navigate( + categoryListUrl({ + ...getActiveFilters(params), + ...filter, + activeTab: undefined + }) + ); + }; + + const closeModal = () => + navigate( + categoryListUrl({ + ...params, + action: undefined, + ids: undefined + }), + true + ); + + const openModal = (action: CategoryListUrlDialog, ids?: string[]) => + navigate( + categoryListUrl({ + ...params, + action, + ids + }) + ); + + const handleTabChange = (tab: number) => { + reset(); + navigate( + categoryListUrl({ + activeTab: tab.toString(), + ...getFilterTabs()[tab - 1].data + }) + ); + }; + + const handleTabDelete = () => { + deleteFilterTab(currentTab); + reset(); + navigate(categoryListUrl()); + }; + + const handleTabSave = (data: SaveFilterTabDialogFormData) => { + saveFilterTab(data.name, getActiveFilters(params)); + handleTabChange(tabs.length + 1); + }; + const paginationState = createPaginationState(settings.rowNumber, params); + const queryVariables = React.useMemo( + () => ({ + ...paginationState, + filter: getFilterVariables(params) + }), + [params] + ); + return ( - + {({ data, loading, refetch }) => { const { loadNextPage, loadPreviousPage, pageInfo } = paginate( maybe(() => data.categories.pageInfo), @@ -78,6 +160,14 @@ export const CategoryList: React.StatelessComponent = ({ () => data.categories.edges.map(edge => edge.node), [] )} + currentTab={currentTab} + initialSearch={params.query || ""} + onSearchChange={query => changeFilterField({ query })} + onAll={() => navigate(categoryListUrl())} + onTabChange={handleTabChange} + onTabDelete={() => openModal("delete-search")} + onTabSave={() => openModal("save-search")} + tabs={tabs.map(tab => tab.name)} settings={settings} onAdd={() => navigate(categoryAddUrl())} onRowClick={id => () => navigate(categoryUrl(id))} @@ -134,7 +224,7 @@ export const CategoryList: React.StatelessComponent = ({ > = ({ + + tabs[currentTab - 1].name, "...")} + /> ); }} diff --git a/src/categories/views/CategoryList/filter.ts b/src/categories/views/CategoryList/filter.ts new file mode 100644 index 000000000..a1e3fa703 --- /dev/null +++ b/src/categories/views/CategoryList/filter.ts @@ -0,0 +1,31 @@ +import { CategoryFilterInput } from "@saleor/types/globalTypes"; +import { + createFilterTabUtils, + createFilterUtils +} from "../../../utils/filters"; +import { + CategoryListUrlFilters, + CategoryListUrlFiltersEnum, + CategoryListUrlQueryParams +} from "../../urls"; + +export const PRODUCT_FILTERS_KEY = "productFilters"; + +export function getFilterVariables( + params: CategoryListUrlFilters +): CategoryFilterInput { + return { + search: params.query + }; +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(PRODUCT_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + CategoryListUrlQueryParams, + CategoryListUrlFilters +>(CategoryListUrlFiltersEnum); diff --git a/src/categories/views/CategoryList/index.ts b/src/categories/views/CategoryList/index.ts new file mode 100644 index 000000000..52c4017ff --- /dev/null +++ b/src/categories/views/CategoryList/index.ts @@ -0,0 +1,2 @@ +export { default } from "./CategoryList"; +export * from "./CategoryList"; diff --git a/src/storybook/stories/categories/CategoryListPage.tsx b/src/storybook/stories/categories/CategoryListPage.tsx index 4fa805709..d488c2175 100644 --- a/src/storybook/stories/categories/CategoryListPage.tsx +++ b/src/storybook/stories/categories/CategoryListPage.tsx @@ -3,7 +3,12 @@ import React from "react"; import CategoryListPage from "../../../categories/components/CategoryListPage"; import { categories } from "../../../categories/fixtures"; -import { listActionsProps, pageListProps } from "../../../fixtures"; +import { + listActionsProps, + pageListProps, + searchPageProps, + tabPageProps +} from "../../../fixtures"; import Decorator from "../../Decorator"; const categoryTableProps = { @@ -11,6 +16,8 @@ const categoryTableProps = { onAddCategory: undefined, onCategoryClick: () => undefined, ...listActionsProps, + ...tabPageProps, + ...searchPageProps, ...pageListProps.default }; diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index e5aacd610..fc5f03ddd 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -316,6 +316,10 @@ export interface CatalogueInput { collections?: (string | null)[] | null; } +export interface CategoryFilterInput { + search?: string | null; +} + export interface CategoryInput { description?: string | null; descriptionJson?: any | null;