Add search bar to category list

This commit is contained in:
dominik-zeglen 2019-09-11 14:59:41 +02:00
parent b842e85759
commit 53ac48062f
13 changed files with 442 additions and 224 deletions

View file

@ -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<typeof styles> {
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 (
<Card>
{!isRoot && (
<CardTitle
title={intl.formatMessage({
defaultMessage: "All Subcategories",
description: "section header"
})}
toolbar={
<Button color="primary" variant="text" onClick={onAdd}>
<FormattedMessage
defaultMessage="Create subcategory"
description="button"
/>
</Button>
}
}: CategoryListProps & WithStyles<typeof styles>) => (
<Table>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={categories}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell className={classes.colName}>
<FormattedMessage defaultMessage="Category Name" />
</TableCell>
<TableCell className={classes.colSubcategories}>
<FormattedMessage
defaultMessage="Subcategories"
description="number of subcategories"
/>
)}
<Table>
<TableHead
</TableCell>
<TableCell className={classes.colProducts}>
<FormattedMessage
defaultMessage="No. of Products"
description="number of products"
/>
</TableCell>
</TableHead>
<TableFooter>
<TableRow>
<TablePagination
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={categories}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell className={classes.colName}>
<FormattedMessage defaultMessage="Category Name" />
</TableCell>
<TableCell className={classes.colSubcategories}>
<FormattedMessage
defaultMessage="Subcategories"
description="number of subcategories"
/>
</TableCell>
<TableCell className={classes.colProducts}>
<FormattedMessage
defaultMessage="No. of Products"
description="number of products"
/>
</TableCell>
</TableHead>
<TableFooter>
<TableRow>
<TablePagination
colSpan={numberOfColumns}
settings={settings}
hasNextPage={
pageInfo && !disabled ? pageInfo.hasNextPage : false
}
onNextPage={onNextPage}
onUpdateListSettings={onUpdateListSettings}
hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
}
onPreviousPage={onPreviousPage}
/>
</TableRow>
</TableFooter>
<TableBody>
{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}
/>
</TableRow>
</TableFooter>
<TableBody>
{renderCollection(
categories,
category => {
const isSelected = category ? isChecked(category.id) : false;
return (
<TableRow
className={classes.tableRow}
hover={!!category}
onClick={category ? onRowClick(category.id) : undefined}
key={category ? category.id : "skeleton"}
selected={isSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(category.id)}
/>
</TableCell>
<TableCell className={classes.colName}>
{category && category.name ? category.name : <Skeleton />}
</TableCell>
<TableCell className={classes.colSubcategories}>
{category &&
category.children &&
category.children.totalCount !== undefined ? (
category.children.totalCount
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colProducts}>
{category &&
category.products &&
category.products.totalCount !== undefined ? (
category.products.totalCount
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
{isRoot ? (
<FormattedMessage defaultMessage="No categories found" />
) : (
<FormattedMessage defaultMessage="No subcategories found" />
)}
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</Card>
);
}
return (
<TableRow
className={classes.tableRow}
hover={!!category}
onClick={category ? onRowClick(category.id) : undefined}
key={category ? category.id : "skeleton"}
selected={isSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(category.id)}
/>
</TableCell>
<TableCell className={classes.colName}>
{category && category.name ? category.name : <Skeleton />}
</TableCell>
<TableCell className={classes.colSubcategories}>
{category &&
category.children &&
category.children.totalCount !== undefined ? (
category.children.totalCount
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colProducts}>
{category &&
category.products &&
category.products.totalCount !== undefined ? (
category.products.totalCount
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
{isRoot ? (
<FormattedMessage defaultMessage="No categories found" />
) : (
<FormattedMessage defaultMessage="No subcategories found" />
)}
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
)
);
CategoryList.displayName = "CategoryList";
export default CategoryList;

View file

@ -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<CategoryTableProps> = ({
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 (
<Container>
<PageHeader title={intl.formatMessage(sectionNames.categories)}>
@ -49,23 +60,38 @@ export const CategoryListPage: React.StatelessComponent<CategoryTableProps> = ({
/>
</Button>
</PageHeader>
<CategoryList
categories={categories}
onAdd={onAdd}
onRowClick={onRowClick}
disabled={disabled}
settings={settings}
isRoot={true}
onNextPage={onNextPage}
onPreviousPage={onPreviousPage}
onUpdateListSettings={onUpdateListSettings}
pageInfo={pageInfo}
isChecked={isChecked}
selected={selected}
toggle={toggle}
toggleAll={toggleAll}
toolbar={toolbar}
/>
<Card>
<SearchBar
currentTab={currentTab}
initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage({
defaultMessage: "Search Attribute"
})}
tabs={tabs}
onAll={onAll}
onSearchChange={onSearchChange}
onTabChange={onTabChange}
onTabDelete={onTabDelete}
onTabSave={onTabSave}
/>
<CategoryList
categories={categories}
disabled={disabled}
isChecked={isChecked}
isRoot={true}
pageInfo={pageInfo}
selected={selected}
settings={settings}
toggle={toggle}
toggleAll={toggleAll}
toolbar={toolbar}
onAdd={onAdd}
onNextPage={onNextPage}
onPreviousPage={onPreviousPage}
onRowClick={onRowClick}
onUpdateListSettings={onUpdateListSettings}
/>
</Card>
</Container>
);
};

View file

@ -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<
</TabContainer>
<CardSpacer />
{currentTab === CategoryPageTab.categories && (
<CategoryList
disabled={disabled}
isRoot={false}
categories={subcategories}
onAdd={onAddCategory}
onRowClick={onCategoryClick}
onNextPage={onNextPage}
onPreviousPage={onPreviousPage}
pageInfo={pageInfo}
toggle={toggle}
toggleAll={toggleAll}
selected={selected}
isChecked={isChecked}
toolbar={subcategoryListToolbar}
/>
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "All Subcategories",
description: "section header"
})}
toolbar={
<Button
color="primary"
variant="text"
onClick={onAddCategory}
>
<FormattedMessage
defaultMessage="Create subcategory"
description="button"
/>
</Button>
}
/>
<CategoryList
categories={subcategories}
disabled={disabled}
isChecked={isChecked}
isRoot={false}
pageInfo={pageInfo}
selected={selected}
toggle={toggle}
toggleAll={toggleAll}
toolbar={subcategoryListToolbar}
onNextPage={onNextPage}
onPreviousPage={onPreviousPage}
onRowClick={onCategoryClick}
/>
</Card>
)}
{currentTab === CategoryPageTab.products && (
<CategoryProducts

View file

@ -1,64 +1,83 @@
import { content } from "../storybook/stories/components/RichTextEditor";
import { CategoryDetails_category } from "./types/CategoryDetails";
import { CategoryFragment } from "./types/CategoryFragment";
export const categories = [
export const categories: CategoryFragment[] = [
{
__typename: "Category",
children: {
__typename: "CategoryCountableConnection",
totalCount: 2
},
id: "123123",
name: "Lorem ipsum dolor",
products: {
__typename: "ProductCountableConnection",
totalCount: 4
}
},
{
__typename: "Category",
children: {
__typename: "CategoryCountableConnection",
totalCount: 54
},
id: "876752",
name: "Mauris vehicula tortor vulputate",
products: {
__typename: "ProductCountableConnection",
totalCount: 3
}
},
{
__typename: "Category",
children: {
__typename: "CategoryCountableConnection",
totalCount: 2
},
id: "876542",
name: "Excepteur sint occaecat cupidatat non proident",
products: {
__typename: "ProductCountableConnection",
totalCount: 6
}
},
{
__typename: "Category",
children: {
__typename: "CategoryCountableConnection",
totalCount: 6
},
id: "875352",
name: "Ut enim ad minim veniam",
products: {
__typename: "ProductCountableConnection",
totalCount: 12
}
},
{
__typename: "Category",
children: {
__typename: "CategoryCountableConnection",
totalCount: 76
},
id: "865752",
name: "Duis aute irure dolor in reprehenderit",
products: {
__typename: "ProductCountableConnection",
totalCount: 43
}
},
{
__typename: "Category",
children: {
__typename: "CategoryCountableConnection",
totalCount: 11
},
id: "878752",
name: "Neque porro quisquam est",
products: {
__typename: "ProductCountableConnection",
totalCount: 21
}
}

View file

@ -7,6 +7,18 @@ import {
} from "./types/CategoryDetails";
import { RootCategories } from "./types/RootCategories";
export const categoryFragment = gql`
fragment CategoryFragment on Category {
id
name
children {
totalCount
}
products {
totalCount
}
}
`;
export const categoryDetailsFragment = gql`
fragment CategoryDetailsFragment on Category {
id
@ -25,11 +37,13 @@ export const categoryDetailsFragment = gql`
`;
export const rootCategories = gql`
${categoryFragment}
query RootCategories(
$first: Int
$after: String
$last: Int
$before: String
$filter: CategoryFilterInput
) {
categories(
level: 0
@ -37,17 +51,11 @@ export const rootCategories = gql`
after: $after
last: $last
before: $before
filter: $filter
) {
edges {
node {
id
name
children {
totalCount
}
products {
totalCount
}
...CategoryFragment
}
}
pageInfo {
@ -64,6 +72,7 @@ export const TypedRootCategoriesQuery = TypedQuery<RootCategories, {}>(
);
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
}
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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<CategoryListUrlFiltersEnum>;
export type CategoryListUrlDialog = "delete" | TabActionDialog;
export type CategoryListUrlQueryParams = ActiveTab &
BulkAction &
CategoryListUrlFilters &
Dialog<CategoryListUrlDialog> &
Pagination;
export const categoryListUrl = (params?: CategoryListUrlQueryParams) =>

View file

@ -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<CategoryListProps> = ({
);
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 (
<TypedRootCategoriesQuery displayLoader variables={paginationState}>
<TypedRootCategoriesQuery displayLoader variables={queryVariables}>
{({ data, loading, refetch }) => {
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
maybe(() => data.categories.pageInfo),
@ -78,6 +160,14 @@ export const CategoryList: React.StatelessComponent<CategoryListProps> = ({
() => 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<CategoryListProps> = ({
>
<FormattedMessage
defaultMessage="Are you sure you want to delete {counter, plural,
one {this attribute}
one {this category}
other {{displayQuantity} categories}
}?"
values={{
@ -148,6 +238,19 @@ export const CategoryList: React.StatelessComponent<CategoryListProps> = ({
<FormattedMessage defaultMessage="Remember this will also delete all products assigned to this category." />
</DialogContentText>
</ActionDialog>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabSave}
/>
<DeleteFilterTabDialog
open={params.action === "delete-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
/>
</>
);
}}

View file

@ -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<CategoryListUrlFilters>(PRODUCT_FILTERS_KEY);
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
CategoryListUrlQueryParams,
CategoryListUrlFilters
>(CategoryListUrlFiltersEnum);

View file

@ -0,0 +1,2 @@
export { default } from "./CategoryList";
export * from "./CategoryList";

View file

@ -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
};

View file

@ -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;