Add search to product types

This commit is contained in:
dominik-zeglen 2019-09-12 15:00:25 +02:00
parent f9285cec60
commit 490c3b7543
8 changed files with 341 additions and 154 deletions

View file

@ -1,4 +1,3 @@
import Card from "@material-ui/core/Card";
import { import {
createStyles, createStyles,
Theme, Theme,
@ -70,138 +69,132 @@ const ProductTypeList = withStyles(styles, { name: "ProductTypeList" })(
const intl = useIntl(); const intl = useIntl();
return ( return (
<Card> <Table>
<Table> <TableHead
<TableHead colSpan={numberOfColumns}
colSpan={numberOfColumns} selected={selected}
selected={selected} disabled={disabled}
disabled={disabled} items={productTypes}
items={productTypes} toggleAll={toggleAll}
toggleAll={toggleAll} toolbar={toolbar}
toolbar={toolbar} >
> <TableCell className={classes.colName}>
<TableCell className={classes.colName}> <FormattedMessage
<FormattedMessage defaultMessage="Type Name"
defaultMessage="Type Name" description="product type name"
description="product type name" />
/> </TableCell>
</TableCell> <TableCell className={classes.colType}>
<TableCell className={classes.colType}> <FormattedMessage
<FormattedMessage defaultMessage="Type"
defaultMessage="Type" description="product type is either simple or configurable"
description="product type is either simple or configurable" />
/> </TableCell>
</TableCell> <TableCell className={classes.colTax}>
<TableCell className={classes.colTax}> <FormattedMessage
<FormattedMessage defaultMessage="Tax"
defaultMessage="Tax" description="tax rate for a product type"
description="tax rate for a product type" />
/> </TableCell>
</TableCell> </TableHead>
</TableHead> <TableFooter>
<TableFooter> <TableRow>
<TableRow> <TablePagination
<TablePagination colSpan={numberOfColumns}
colSpan={numberOfColumns} hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
hasNextPage={ onNextPage={onNextPage}
pageInfo && !disabled ? pageInfo.hasNextPage : false hasPreviousPage={
} pageInfo && !disabled ? pageInfo.hasPreviousPage : false
onNextPage={onNextPage} }
hasPreviousPage={ onPreviousPage={onPreviousPage}
pageInfo && !disabled ? pageInfo.hasPreviousPage : false />
} </TableRow>
onPreviousPage={onPreviousPage} </TableFooter>
/> <TableBody>
</TableRow> {renderCollection(
</TableFooter> productTypes,
<TableBody> productType => {
{renderCollection( const isSelected = productType
productTypes, ? isChecked(productType.id)
productType => { : false;
const isSelected = productType return (
? isChecked(productType.id) <TableRow
: false; className={!!productType ? classes.link : undefined}
return ( hover={!!productType}
<TableRow key={productType ? productType.id : "skeleton"}
className={!!productType ? classes.link : undefined} onClick={productType ? onRowClick(productType.id) : undefined}
hover={!!productType} selected={isSelected}
key={productType ? productType.id : "skeleton"} >
onClick={ <TableCell padding="checkbox">
productType ? onRowClick(productType.id) : undefined <Checkbox
} checked={isSelected}
selected={isSelected} disabled={disabled}
> disableClickPropagation
<TableCell padding="checkbox"> onChange={() => toggle(productType.id)}
<Checkbox />
checked={isSelected} </TableCell>
disabled={disabled} <TableCell className={classes.colName}>
disableClickPropagation {productType ? (
onChange={() => toggle(productType.id)} <>
/> {productType.name}
</TableCell> <Typography variant="caption">
<TableCell className={classes.colName}> {maybe(() => productType.hasVariants)
{productType ? ( ? intl.formatMessage({
defaultMessage: "Configurable",
description: "product type"
})
: intl.formatMessage({
defaultMessage: "Simple product",
description: "product type"
})}
</Typography>
</>
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colType}>
{maybe(() => productType.isShippingRequired) !==
undefined ? (
productType.isShippingRequired ? (
<> <>
{productType.name} <FormattedMessage
<Typography variant="caption"> defaultMessage="Physical"
{maybe(() => productType.hasVariants) description="product type"
? intl.formatMessage({ />
defaultMessage: "Configurable",
description: "product type"
})
: intl.formatMessage({
defaultMessage: "Simple product",
description: "product type"
})}
</Typography>
</> </>
) : ( ) : (
<Skeleton /> <>
)} <FormattedMessage
</TableCell> defaultMessage="Digital"
<TableCell className={classes.colType}> description="product type"
{maybe(() => productType.isShippingRequired) !== />
undefined ? ( </>
productType.isShippingRequired ? ( )
<> ) : (
<FormattedMessage <Skeleton />
defaultMessage="Physical" )}
description="product type" </TableCell>
/> <TableCell className={classes.colTax}>
</> {maybe(() => productType.taxType) ? (
) : ( productType.taxType.description
<> ) : (
<FormattedMessage <Skeleton />
defaultMessage="Digital" )}
description="product type"
/>
</>
)
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colTax}>
{maybe(() => productType.taxType) ? (
productType.taxType.description
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No product types found" />
</TableCell> </TableCell>
</TableRow> </TableRow>
) );
)} },
</TableBody> () => (
</Table> <TableRow>
</Card> <TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No product types found" />
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
); );
} }
); );

View file

@ -1,23 +1,46 @@
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import React from "react"; 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 Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SearchBar from "@saleor/components/SearchBar";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { ListActions, PageListProps } from "../../../types"; import {
ListActions,
PageListProps,
SearchPageProps,
TabPageProps
} from "../../../types";
import { ProductTypeList_productTypes_edges_node } from "../../types/ProductTypeList"; import { ProductTypeList_productTypes_edges_node } from "../../types/ProductTypeList";
import ProductTypeList from "../ProductTypeList"; import ProductTypeList from "../ProductTypeList";
interface ProductTypeListPageProps extends PageListProps, ListActions { interface ProductTypeListPageProps
extends PageListProps,
ListActions,
SearchPageProps,
TabPageProps {
productTypes: ProductTypeList_productTypes_edges_node[]; productTypes: ProductTypeList_productTypes_edges_node[];
onBack: () => void; onBack: () => void;
} }
const ProductTypeListPage: React.StatelessComponent< const ProductTypeListPage: React.StatelessComponent<
ProductTypeListPageProps ProductTypeListPageProps
> = ({ disabled, onAdd, onBack, ...listProps }) => { > = ({
currentTab,
initialSearch,
onAdd,
onAll,
onBack,
onSearchChange,
onTabChange,
onTabDelete,
onTabSave,
tabs,
...listProps
}) => {
const intl = useIntl(); const intl = useIntl();
return ( return (
@ -26,19 +49,33 @@ const ProductTypeListPage: React.StatelessComponent<
{intl.formatMessage(sectionNames.configuration)} {intl.formatMessage(sectionNames.configuration)}
</AppHeader> </AppHeader>
<PageHeader title={intl.formatMessage(sectionNames.productTypes)}> <PageHeader title={intl.formatMessage(sectionNames.productTypes)}>
<Button <Button color="primary" variant="contained" onClick={onAdd}>
color="primary"
variant="contained"
disabled={disabled}
onClick={onAdd}
>
<FormattedMessage <FormattedMessage
defaultMessage="create product type" defaultMessage="create product type"
description="button" description="button"
/> />
</Button> </Button>
</PageHeader> </PageHeader>
<ProductTypeList disabled={disabled} {...listProps} /> <Card>
<SearchBar
allTabLabel={intl.formatMessage({
defaultMessage: "All Product Types",
description: "tab name"
})}
currentTab={currentTab}
initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage({
defaultMessage: "Search Product Type"
})}
tabs={tabs}
onAll={onAll}
onSearchChange={onSearchChange}
onTabChange={onTabChange}
onTabDelete={onTabDelete}
onTabSave={onTabSave}
/>
<ProductTypeList {...listProps} />
</Card>
</Container> </Container>
); );
}; };

View file

@ -51,8 +51,15 @@ export const productTypeListQuery = gql`
$before: String $before: String
$first: Int $first: Int
$last: Int $last: Int
$filter: ProductTypeFilterInput
) { ) {
productTypes(after: $after, before: $before, first: $first, last: $last) { productTypes(
after: $after
before: $before
first: $first
last: $last
filter: $filter
) {
edges { edges {
node { node {
...ProductTypeFragment ...ProductTypeFragment

View file

@ -1,15 +1,29 @@
import { stringify as stringifyQs } from "qs"; import { stringify as stringifyQs } from "qs";
import urlJoin from "url-join"; import urlJoin from "url-join";
import { BulkAction, Dialog, Pagination, SingleAction } from "../types"; import {
ActiveTab,
BulkAction,
Dialog,
Filters,
Pagination,
SingleAction,
TabActionDialog
} from "../types";
const productTypeSection = "/product-types/"; const productTypeSection = "/product-types/";
export const productTypeListPath = productTypeSection; export const productTypeListPath = productTypeSection;
export type ProductTypeListUrlDialog = "remove"; export enum ProductTypeListUrlFiltersEnum {
export type ProductTypeListUrlQueryParams = BulkAction & query = "query"
}
export type ProductTypeListUrlFilters = Filters<ProductTypeListUrlFiltersEnum>;
export type ProductTypeListUrlDialog = "remove" | TabActionDialog;
export type ProductTypeListUrlQueryParams = ActiveTab &
BulkAction &
Dialog<ProductTypeListUrlDialog> & Dialog<ProductTypeListUrlDialog> &
Pagination; Pagination &
ProductTypeListUrlFilters;
export const productTypeListUrl = (params?: ProductTypeListUrlQueryParams) => export const productTypeListUrl = (params?: ProductTypeListUrlQueryParams) =>
productTypeListPath + "?" + stringifyQs(params); productTypeListPath + "?" + stringifyQs(params);

View file

@ -5,26 +5,41 @@ import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import ActionDialog from "@saleor/components/ActionDialog"; 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 useBulkActions from "@saleor/hooks/useBulkActions";
import useListSettings from "@saleor/hooks/useListSettings";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import usePaginator, { import usePaginator, {
createPaginationState createPaginationState
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { PAGINATE_BY } from "../../config"; import { ListViews } from "@saleor/types";
import { configurationMenuUrl } from "../../configuration"; import { configurationMenuUrl } from "../../../configuration";
import { getMutationState, maybe } from "../../misc"; import { getMutationState, maybe } from "../../../misc";
import ProductTypeListPage from "../components/ProductTypeListPage"; import ProductTypeListPage from "../../components/ProductTypeListPage";
import { TypedProductTypeBulkDeleteMutation } from "../mutations"; import { TypedProductTypeBulkDeleteMutation } from "../../mutations";
import { TypedProductTypeListQuery } from "../queries"; import { TypedProductTypeListQuery } from "../../queries";
import { ProductTypeBulkDelete } from "../types/ProductTypeBulkDelete"; import { ProductTypeBulkDelete } from "../../types/ProductTypeBulkDelete";
import { import {
productTypeAddUrl, productTypeAddUrl,
productTypeListUrl, productTypeListUrl,
ProductTypeListUrlDialog,
ProductTypeListUrlFilters,
ProductTypeListUrlQueryParams, ProductTypeListUrlQueryParams,
productTypeUrl productTypeUrl
} from "../urls"; } from "../../urls";
import {
areFiltersApplied,
deleteFilterTab,
getActiveFilters,
getFilterTabs,
getFilterVariables,
saveFilterTab
} from "./filter";
interface ProductTypeListProps { interface ProductTypeListProps {
params: ProductTypeListUrlQueryParams; params: ProductTypeListUrlQueryParams;
@ -39,13 +54,79 @@ export const ProductTypeList: React.StatelessComponent<
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids params.ids
); );
const { settings } = useListSettings(ListViews.PRODUCT_LIST);
const intl = useIntl(); const intl = useIntl();
const closeModal = () => navigate(productTypeListUrl(), true); const tabs = getFilterTabs();
const currentTab =
params.activeTab === undefined
? areFiltersApplied(params)
? tabs.length + 1
: 0
: parseInt(params.activeTab, 0);
const changeFilterField = (filter: ProductTypeListUrlFilters) => {
reset();
navigate(
productTypeListUrl({
...getActiveFilters(params),
...filter,
activeTab: undefined
})
);
};
const closeModal = () =>
navigate(
productTypeListUrl({
...params,
action: undefined,
ids: undefined
})
);
const openModal = (action: ProductTypeListUrlDialog, ids?: string[]) =>
navigate(
productTypeListUrl({
...params,
action,
ids
})
);
const handleTabChange = (tab: number) => {
reset();
navigate(
productTypeListUrl({
activeTab: tab.toString(),
...getFilterTabs()[tab - 1].data
})
);
};
const handleTabDelete = () => {
deleteFilterTab(currentTab);
reset();
navigate(productTypeListUrl());
};
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]
);
const paginationState = createPaginationState(PAGINATE_BY, params);
return ( return (
<TypedProductTypeListQuery displayLoader variables={paginationState}> <TypedProductTypeListQuery displayLoader variables={queryVariables}>
{({ data, loading, refetch }) => { {({ data, loading, refetch }) => {
const { loadNextPage, loadPreviousPage, pageInfo } = paginate( const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
maybe(() => data.productTypes.pageInfo), maybe(() => data.productTypes.pageInfo),
@ -93,6 +174,14 @@ export const ProductTypeList: React.StatelessComponent<
return ( return (
<> <>
<ProductTypeListPage <ProductTypeListPage
currentTab={currentTab}
initialSearch={params.query || ""}
onSearchChange={query => changeFilterField({ query })}
onAll={() => navigate(productTypeListUrl())}
onTabChange={handleTabChange}
onTabDelete={() => openModal("delete-search")}
onTabSave={() => openModal("save-search")}
tabs={tabs.map(tab => tab.name)}
disabled={loading} disabled={loading}
productTypes={maybe(() => productTypes={maybe(() =>
data.productTypes.edges.map(edge => edge.node) data.productTypes.edges.map(edge => edge.node)
@ -150,6 +239,19 @@ export const ProductTypeList: React.StatelessComponent<
/> />
</DialogContentText> </DialogContentText>
</ActionDialog> </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 { ProductTypeFilterInput } from "@saleor/types/globalTypes";
import {
createFilterTabUtils,
createFilterUtils
} from "../../../utils/filters";
import {
ProductTypeListUrlFilters,
ProductTypeListUrlFiltersEnum,
ProductTypeListUrlQueryParams
} from "../../urls";
export const PRODUCT_TYPE_FILTERS_KEY = "productTypeFilters";
export function getFilterVariables(
params: ProductTypeListUrlFilters
): ProductTypeFilterInput {
return {
search: params.query
};
}
export const {
deleteFilterTab,
getFilterTabs,
saveFilterTab
} = createFilterTabUtils<ProductTypeListUrlFilters>(PRODUCT_TYPE_FILTERS_KEY);
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
ProductTypeListUrlQueryParams,
ProductTypeListUrlFilters
>(ProductTypeListUrlFiltersEnum);

View file

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

View file

@ -24,6 +24,7 @@ export enum ListViews {
PAGES_LIST = "PAGES_LIST", PAGES_LIST = "PAGES_LIST",
PLUGINS_LIST = "PLUGIN_LIST", PLUGINS_LIST = "PLUGIN_LIST",
PRODUCT_LIST = "PRODUCT_LIST", PRODUCT_LIST = "PRODUCT_LIST",
PRODUCT_TYPE_LIST = "PRODUCT_TYPE_LIST",
SALES_LIST = "SALES_LIST", SALES_LIST = "SALES_LIST",
SHIPPING_METHODS_LIST = "SHIPPING_METHODS_LIST", SHIPPING_METHODS_LIST = "SHIPPING_METHODS_LIST",
STAFF_MEMBERS_LIST = "STAFF_MEMBERS_LIST", STAFF_MEMBERS_LIST = "STAFF_MEMBERS_LIST",