Add search to collection list

This commit is contained in:
dominik-zeglen 2019-09-11 16:00:02 +02:00
parent 53ac48062f
commit 89e7aa82df
11 changed files with 308 additions and 130 deletions

View file

@ -65,7 +65,7 @@ export const CategoryListPage: React.StatelessComponent<CategoryTableProps> = ({
currentTab={currentTab} currentTab={currentTab}
initialSearch={initialSearch} initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage({ searchPlaceholder={intl.formatMessage({
defaultMessage: "Search Attribute" defaultMessage: "Search Category"
})} })}
tabs={tabs} tabs={tabs}
onAll={onAll} onAll={onAll}

View file

@ -9,7 +9,7 @@ import {
CategoryListUrlQueryParams CategoryListUrlQueryParams
} from "../../urls"; } from "../../urls";
export const PRODUCT_FILTERS_KEY = "productFilters"; export const CATEGORY_FILTERS_KEY = "categoryFilters";
export function getFilterVariables( export function getFilterVariables(
params: CategoryListUrlFilters params: CategoryListUrlFilters
@ -23,7 +23,7 @@ export const {
deleteFilterTab, deleteFilterTab,
getFilterTabs, getFilterTabs,
saveFilterTab saveFilterTab
} = createFilterTabUtils<CategoryListUrlFilters>(PRODUCT_FILTERS_KEY); } = createFilterTabUtils<CategoryListUrlFilters>(CATEGORY_FILTERS_KEY);
export const { areFiltersApplied, getActiveFilters } = createFilterUtils< export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
CategoryListUrlQueryParams, CategoryListUrlQueryParams,

View file

@ -1,4 +1,3 @@
import Card from "@material-ui/core/Card";
import { import {
createStyles, createStyles,
Theme, Theme,
@ -74,118 +73,110 @@ const CollectionList = withStyles(styles, { name: "CollectionList" })(
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={collections}
items={collections} toggleAll={toggleAll}
toggleAll={toggleAll} toolbar={toolbar}
toolbar={toolbar} >
> <TableCell className={classes.colName}>
<TableCell className={classes.colName}> <FormattedMessage defaultMessage="Category Name" />
<FormattedMessage defaultMessage="Category Name" /> </TableCell>
</TableCell> <TableCell className={classes.colProducts}>
<TableCell className={classes.colProducts}> <FormattedMessage defaultMessage="No. of Products" />
<FormattedMessage defaultMessage="No. of Products" /> </TableCell>
</TableCell> <TableCell className={classes.colAvailability}>
<TableCell className={classes.colAvailability}> <FormattedMessage
<FormattedMessage defaultMessage="Availability"
defaultMessage="Availability" description="collection availability"
description="collection availability" />
/> </TableCell>
</TableCell> </TableHead>
</TableHead> <TableFooter>
<TableFooter> <TableRow>
<TableRow> <TablePagination
<TablePagination colSpan={numberOfColumns}
colSpan={numberOfColumns} settings={settings}
settings={settings} hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
hasNextPage={ onNextPage={onNextPage}
pageInfo && !disabled ? pageInfo.hasNextPage : false onUpdateListSettings={onUpdateListSettings}
} hasPreviousPage={
onNextPage={onNextPage} pageInfo && !disabled ? pageInfo.hasPreviousPage : false
onUpdateListSettings={onUpdateListSettings} }
hasPreviousPage={ onPreviousPage={onPreviousPage}
pageInfo && !disabled ? pageInfo.hasPreviousPage : false />
} </TableRow>
onPreviousPage={onPreviousPage} </TableFooter>
/> <TableBody>
</TableRow> {renderCollection(
</TableFooter> collections,
<TableBody> collection => {
{renderCollection( const isSelected = collection ? isChecked(collection.id) : false;
collections, return (
collection => { <TableRow
const isSelected = collection className={classes.tableRow}
? isChecked(collection.id) hover={!!collection}
: false; onClick={collection ? onRowClick(collection.id) : undefined}
return ( key={collection ? collection.id : "skeleton"}
<TableRow selected={isSelected}
className={classes.tableRow} >
hover={!!collection} <TableCell padding="checkbox">
onClick={collection ? onRowClick(collection.id) : undefined} <Checkbox
key={collection ? collection.id : "skeleton"} checked={isSelected}
selected={isSelected} disabled={disabled}
> disableClickPropagation
<TableCell padding="checkbox"> onChange={() => toggle(collection.id)}
<Checkbox />
checked={isSelected} </TableCell>
disabled={disabled} <TableCell className={classes.colName}>
disableClickPropagation {maybe<React.ReactNode>(
onChange={() => toggle(collection.id)} () => collection.name,
/> <Skeleton />
</TableCell> )}
<TableCell className={classes.colName}> </TableCell>
{maybe<React.ReactNode>( <TableCell className={classes.colProducts}>
() => collection.name, {maybe<React.ReactNode>(
<Skeleton /> () => collection.products.totalCount,
)} <Skeleton />
</TableCell> )}
<TableCell className={classes.colProducts}> </TableCell>
{maybe<React.ReactNode>( <TableCell className={classes.colAvailability}>
() => collection.products.totalCount, {maybe(
<Skeleton /> () => (
)} <StatusLabel
</TableCell> status={collection.isPublished ? "success" : "error"}
<TableCell className={classes.colAvailability}> label={
{maybe( collection.isPublished
() => ( ? intl.formatMessage({
<StatusLabel defaultMessage: "Published",
status={ description: "collection is published"
collection.isPublished ? "success" : "error" })
} : intl.formatMessage({
label={ defaultMessage: "Not published",
collection.isPublished description: "collection is not published"
? intl.formatMessage({ })
defaultMessage: "Published", }
description: "collection is published" />
}) ),
: intl.formatMessage({ <Skeleton />
defaultMessage: "Not published", )}
description: "collection is not published"
})
}
/>
),
<Skeleton />
)}
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No collections found" />
</TableCell> </TableCell>
</TableRow> </TableRow>
) );
)} },
</TableBody> () => (
</Table> <TableRow>
</Card> <TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No collections found" />
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
); );
} }
); );

View file

@ -1,22 +1,40 @@
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 { 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 "@saleor/types"; import {
ListActions,
PageListProps,
SearchPageProps,
TabPageProps
} from "@saleor/types";
import { CollectionList_collections_edges_node } from "../../types/CollectionList"; import { CollectionList_collections_edges_node } from "../../types/CollectionList";
import CollectionList from "../CollectionList/CollectionList"; import CollectionList from "../CollectionList/CollectionList";
export interface CollectionListPageProps extends PageListProps, ListActions { export interface CollectionListPageProps
extends PageListProps,
ListActions,
SearchPageProps,
TabPageProps {
collections: CollectionList_collections_edges_node[]; collections: CollectionList_collections_edges_node[];
} }
const CollectionListPage: React.StatelessComponent<CollectionListPageProps> = ({ const CollectionListPage: React.StatelessComponent<CollectionListPageProps> = ({
currentTab,
disabled, disabled,
initialSearch,
onAdd, onAdd,
onAll,
onSearchChange,
onTabChange,
onTabDelete,
onTabSave,
tabs,
...listProps ...listProps
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@ -36,7 +54,22 @@ const CollectionListPage: React.StatelessComponent<CollectionListPageProps> = ({
/> />
</Button> </Button>
</PageHeader> </PageHeader>
<CollectionList disabled={disabled} {...listProps} /> <Card>
<SearchBar
currentTab={currentTab}
initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage({
defaultMessage: "Search Collection"
})}
tabs={tabs}
onAll={onAll}
onSearchChange={onSearchChange}
onTabChange={onTabChange}
onTabDelete={onTabDelete}
onTabSave={onTabSave}
/>
<CollectionList disabled={disabled} {...listProps} />
</Card>
</Container> </Container>
); );
}; };

View file

@ -60,8 +60,15 @@ export const collectionList = gql`
$after: String $after: String
$last: Int $last: Int
$before: String $before: String
$filter: CollectionFilterInput
) { ) {
collections(first: $first, after: $after, before: $before, last: $last) { collections(
first: $first
after: $after
before: $before
last: $last
filter: $filter
) {
edges { edges {
node { node {
...CollectionFragment ...CollectionFragment

View file

@ -2,6 +2,8 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // This file was automatically generated and should not be edited.
import { CollectionFilterInput } from "./../../types/globalTypes";
// ==================================================== // ====================================================
// GraphQL query operation: CollectionList // GraphQL query operation: CollectionList
// ==================================================== // ====================================================
@ -47,4 +49,5 @@ export interface CollectionListVariables {
after?: string | null; after?: string | null;
last?: number | null; last?: number | null;
before?: string | null; before?: string | null;
filter?: CollectionFilterInput | null;
} }

View file

@ -1,13 +1,30 @@
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 } from "../types"; import {
ActiveTab,
BulkAction,
Dialog,
Filters,
Pagination,
TabActionDialog
} from "../types";
const collectionSectionUrl = "/collections/"; const collectionSectionUrl = "/collections/";
export const collectionListPath = collectionSectionUrl; export const collectionListPath = collectionSectionUrl;
export type CollectionListUrlDialog = "publish" | "unpublish" | "remove"; export enum CollectionListUrlFiltersEnum {
export type CollectionListUrlQueryParams = BulkAction & query = "query"
}
export type CollectionListUrlFilters = Filters<CollectionListUrlFiltersEnum>;
export type CollectionListUrlDialog =
| "publish"
| "unpublish"
| "remove"
| TabActionDialog;
export type CollectionListUrlQueryParams = ActiveTab &
BulkAction &
CollectionListUrlFilters &
Dialog<CollectionListUrlDialog> & Dialog<CollectionListUrlDialog> &
Pagination; Pagination;
export const collectionListUrl = (params?: CollectionListUrlQueryParams) => export const collectionListUrl = (params?: CollectionListUrlQueryParams) =>

View file

@ -5,7 +5,12 @@ import DeleteIcon from "@material-ui/icons/Delete";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { CategoryListUrlFilters } from "@saleor/categories/urls";
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 useListSettings from "@saleor/hooks/useListSettings";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
@ -16,21 +21,29 @@ import usePaginator, {
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { getMutationState, maybe } from "@saleor/misc"; import { getMutationState, maybe } from "@saleor/misc";
import { ListViews } from "@saleor/types"; import { ListViews } from "@saleor/types";
import CollectionListPage from "../components/CollectionListPage/CollectionListPage"; import CollectionListPage from "../../components/CollectionListPage/CollectionListPage";
import { import {
TypedCollectionBulkDelete, TypedCollectionBulkDelete,
TypedCollectionBulkPublish TypedCollectionBulkPublish
} from "../mutations"; } from "../../mutations";
import { TypedCollectionListQuery } from "../queries"; import { TypedCollectionListQuery } from "../../queries";
import { CollectionBulkDelete } from "../types/CollectionBulkDelete"; import { CollectionBulkDelete } from "../../types/CollectionBulkDelete";
import { CollectionBulkPublish } from "../types/CollectionBulkPublish"; import { CollectionBulkPublish } from "../../types/CollectionBulkPublish";
import { import {
collectionAddUrl, collectionAddUrl,
collectionListUrl, collectionListUrl,
CollectionListUrlDialog, CollectionListUrlDialog,
CollectionListUrlQueryParams, CollectionListUrlQueryParams,
collectionUrl collectionUrl
} from "../urls"; } from "../../urls";
import {
areFiltersApplied,
deleteFilterTab,
getActiveFilters,
getFilterTabs,
getFilterVariables,
saveFilterTab
} from "./filter";
interface CollectionListProps { interface CollectionListProps {
params: CollectionListUrlQueryParams; params: CollectionListUrlQueryParams;
@ -50,6 +63,26 @@ export const CollectionList: React.StatelessComponent<CollectionListProps> = ({
); );
const intl = useIntl(); 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(
collectionListUrl({
...getActiveFilters(params),
...filter,
activeTab: undefined
})
);
};
const closeModal = () => const closeModal = () =>
navigate( navigate(
collectionListUrl({ collectionListUrl({
@ -60,17 +93,47 @@ export const CollectionList: React.StatelessComponent<CollectionListProps> = ({
true true
); );
const openModal = (action: CollectionListUrlDialog, ids: string[]) => const openModal = (action: CollectionListUrlDialog, ids?: string[]) =>
navigate( navigate(
collectionListUrl({ collectionListUrl({
...params,
action, action,
ids ids
}) })
); );
const handleTabChange = (tab: number) => {
reset();
navigate(
collectionListUrl({
activeTab: tab.toString(),
...getFilterTabs()[tab - 1].data
})
);
};
const handleTabDelete = () => {
deleteFilterTab(currentTab);
reset();
navigate(collectionListUrl());
};
const handleTabSave = (data: SaveFilterTabDialogFormData) => {
saveFilterTab(data.name, getActiveFilters(params));
handleTabChange(tabs.length + 1);
};
const paginationState = createPaginationState(settings.rowNumber, params); const paginationState = createPaginationState(settings.rowNumber, params);
const queryVariables = React.useMemo(
() => ({
...paginationState,
filter: getFilterVariables(params)
}),
[params]
);
return ( return (
<TypedCollectionListQuery displayLoader variables={paginationState}> <TypedCollectionListQuery displayLoader variables={queryVariables}>
{({ data, loading, refetch }) => { {({ data, loading, refetch }) => {
const { loadNextPage, loadPreviousPage, pageInfo } = paginate( const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
maybe(() => data.collections.pageInfo), maybe(() => data.collections.pageInfo),
@ -130,7 +193,15 @@ export const CollectionList: React.StatelessComponent<CollectionListProps> = ({
return ( return (
<> <>
<CollectionListPage <CollectionListPage
currentTab={currentTab}
initialSearch={params.query || ""}
onSearchChange={query => changeFilterField({ query })}
onAdd={() => navigate(collectionAddUrl)} onAdd={() => navigate(collectionAddUrl)}
onAll={() => navigate(collectionListUrl())}
onTabChange={handleTabChange}
onTabDelete={() => openModal("delete-search")}
onTabSave={() => openModal("save-search")}
tabs={tabs.map(tab => tab.name)}
disabled={loading} disabled={loading}
collections={maybe(() => collections={maybe(() =>
data.collections.edges.map(edge => edge.node) data.collections.edges.map(edge => edge.node)
@ -289,6 +360,19 @@ export const CollectionList: React.StatelessComponent<CollectionListProps> = ({
/> />
</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 { CollectionFilterInput } from "@saleor/types/globalTypes";
import {
createFilterTabUtils,
createFilterUtils
} from "../../../utils/filters";
import {
CollectionListUrlFilters,
CollectionListUrlFiltersEnum,
CollectionListUrlQueryParams
} from "../../urls";
export const COLLECTION_FILTERS_KEY = "collectionFilters";
export function getFilterVariables(
params: CollectionListUrlFilters
): CollectionFilterInput {
return {
search: params.query
};
}
export const {
deleteFilterTab,
getFilterTabs,
saveFilterTab
} = createFilterTabUtils<CollectionListUrlFilters>(COLLECTION_FILTERS_KEY);
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
CollectionListUrlQueryParams,
CollectionListUrlFilters
>(CollectionListUrlFiltersEnum);

View file

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

View file

@ -33,6 +33,11 @@ export enum AuthorizationKeyType {
GOOGLE_OAUTH2 = "GOOGLE_OAUTH2", GOOGLE_OAUTH2 = "GOOGLE_OAUTH2",
} }
export enum CollectionPublished {
HIDDEN = "HIDDEN",
PUBLISHED = "PUBLISHED",
}
export enum ConfigurationTypeFieldEnum { export enum ConfigurationTypeFieldEnum {
BOOLEAN = "BOOLEAN", BOOLEAN = "BOOLEAN",
STRING = "STRING", STRING = "STRING",
@ -343,6 +348,11 @@ export interface CollectionCreateInput {
products?: (string | null)[] | null; products?: (string | null)[] | null;
} }
export interface CollectionFilterInput {
published?: CollectionPublished | null;
search?: string | null;
}
export interface CollectionInput { export interface CollectionInput {
isPublished?: boolean | null; isPublished?: boolean | null;
name?: string | null; name?: string | null;