Merge pull request #1269 from mirumee/SALEOR-3695-collection-list-adjustments

Add filter bar to collection view
This commit is contained in:
SektorDV 2021-08-11 12:09:21 +02:00 committed by GitHub
commit e26d6b5a66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 204 additions and 54 deletions

View file

@ -1,5 +1,7 @@
import { TableBody, TableCell, TableFooter, TableRow } from "@material-ui/core";
import { CollectionListUrlSortField } from "@saleor/collections/urls";
import { canBeSorted } from "@saleor/collections/views/CollectionList/sort";
import AvailabilityStatusLabel from "@saleor/components/AvailabilityStatusLabel";
import { ChannelsAvailabilityDropdown } from "@saleor/components/ChannelsAvailabilityDropdown";
import Checkbox from "@saleor/components/Checkbox";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
@ -15,6 +17,7 @@ import React from "react";
import { FormattedMessage } from "react-intl";
import { CollectionList_collections_edges_node } from "../../types/CollectionList";
import { messages } from "./messages";
const useStyles = makeStyles(
theme => ({
@ -116,6 +119,12 @@ const CollectionList: React.FC<CollectionListProps> = props => {
}
onClick={() => onSort(CollectionListUrlSortField.available)}
className={classes.colAvailability}
disabled={
!canBeSorted(
CollectionListUrlSortField.available,
!!selectedChannelId
)
}
>
<FormattedMessage
defaultMessage="Availability"
@ -178,19 +187,20 @@ const CollectionList: React.FC<CollectionListProps> = props => {
data-test="availability"
data-test-availability={!!collection?.channelListings?.length}
>
{collection && !collection?.channelListings?.length ? (
"-"
) : collection?.channelListings !== undefined ? (
channel ? (
{(!collection && <Skeleton />) ||
(!collection?.channelListings?.length && "-") ||
(collection?.channelListings !== undefined && channel ? (
<AvailabilityStatusLabel
channel={channel}
messages={messages}
/>
) : (
<ChannelsAvailabilityDropdown
allChannelsCount={channelsCount}
channels={collection?.channelListings}
showStatus
/>
) : null
) : (
<Skeleton />
)}
))}
</TableCell>
</TableRow>
);

View file

@ -0,0 +1,16 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
published: {
defaultMessage: "Published on {date}",
description: "collection publication date"
},
unpublished: {
defaultMessage: "Unpublished",
description: "collection publication date"
},
willBePublished: {
defaultMessage: "Becomes published on {date}",
description: "collection publication date"
}
});

View file

@ -1,11 +1,12 @@
import { Button, Card } from "@material-ui/core";
import { CollectionListUrlSortField } from "@saleor/collections/urls";
import { Container } from "@saleor/components/Container";
import FilterBar from "@saleor/components/FilterBar";
import PageHeader from "@saleor/components/PageHeader";
import SearchBar from "@saleor/components/SearchBar";
import { sectionNames } from "@saleor/intl";
import {
ChannelProps,
FilterPageProps,
ListActions,
PageListProps,
SearchPageProps,
@ -17,13 +18,18 @@ import { FormattedMessage, useIntl } from "react-intl";
import { CollectionList_collections_edges_node } from "../../types/CollectionList";
import CollectionList from "../CollectionList/CollectionList";
import {
CollectionFilterKeys,
CollectionListFilterOpts,
createFilterStructure
} from "./filters";
export interface CollectionListPageProps
extends PageListProps,
ListActions,
SearchPageProps,
SortPage<CollectionListUrlSortField>,
TabPageProps,
FilterPageProps<CollectionFilterKeys, CollectionListFilterOpts>,
ChannelProps {
collections: CollectionList_collections_edges_node[];
channelsCount: number;
@ -42,9 +48,13 @@ const CollectionListPage: React.FC<CollectionListPageProps> = ({
onTabSave,
selectedChannelId,
tabs,
filterOpts,
onFilterChange,
onFilterAttributeFocus,
...listProps
}) => {
const intl = useIntl();
const filterStructure = createFilterStructure(intl, filterOpts);
return (
<Container>
@ -63,22 +73,25 @@ const CollectionListPage: React.FC<CollectionListPageProps> = ({
</Button>
</PageHeader>
<Card>
<SearchBar
<FilterBar
allTabLabel={intl.formatMessage({
defaultMessage: "All Collections",
description: "tab name"
})}
currentTab={currentTab}
filterStructure={filterStructure}
initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage({
defaultMessage: "Search Collection"
})}
tabs={tabs}
onAll={onAll}
onFilterChange={onFilterChange}
onFilterAttributeFocus={onFilterAttributeFocus}
onSearchChange={onSearchChange}
onTabChange={onTabChange}
onTabDelete={onTabDelete}
onTabSave={onTabSave}
searchPlaceholder={intl.formatMessage({
defaultMessage: "Search Collections"
})}
tabs={tabs}
/>
<CollectionList
disabled={disabled}

View file

@ -1,4 +1,5 @@
import { IFilter } from "@saleor/components/Filter";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { commonMessages } from "@saleor/intl";
import { FilterOpts } from "@saleor/types";
import { CollectionPublished } from "@saleor/types/globalTypes";
@ -7,10 +8,12 @@ import { defineMessages, IntlShape } from "react-intl";
export interface CollectionListFilterOpts {
status: FilterOpts<CollectionPublished>;
channel: FilterOpts<string> & { choices: MultiAutocompleteChoiceType[] };
}
export enum CollectionFilterKeys {
status = "status"
status = "status",
channel = "channel"
}
const messages = defineMessages({
@ -46,7 +49,18 @@ export function createFilterStructure(
}
]
),
active: opts.status.active
active: opts.status.active,
dependencies: [CollectionFilterKeys.channel]
},
{
...createOptionsField(
CollectionFilterKeys.channel,
intl.formatMessage(commonMessages.channel),
[opts.channel.value],
false,
opts.channel.choices
),
active: opts.channel.active
}
];
}

View file

@ -1,9 +1,29 @@
import { CollectionPublished } from "@saleor/types/globalTypes";
import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json";
import { CollectionListFilterOpts } from "./components/CollectionListPage";
import { CollectionDetails_collection } from "./types/CollectionDetails";
import { CollectionList_collections_edges_node } from "./types/CollectionList";
const content = richTextEditorFixtures.richTextEditor;
export const collectionListFilterOpts: CollectionListFilterOpts = {
channel: {
active: false,
value: "default-channel",
choices: [
{
value: "default-channel",
label: "Default channel"
}
]
},
status: {
active: false,
value: CollectionPublished.PUBLISHED
}
};
export const collections: CollectionList_collections_edges_node[] = [
{
__typename: "Collection",

View file

@ -25,6 +25,7 @@ export const collectionList = gql`
$before: String
$filter: CollectionFilterInput
$sort: CollectionSortingInput
$channel: String
) {
collections(
first: $first
@ -33,6 +34,7 @@ export const collectionList = gql`
last: $last
filter: $filter
sortBy: $sort
channel: $channel
) {
edges {
node {

View file

@ -17,7 +17,8 @@ const collectionSectionUrl = "/collections/";
export const collectionListPath = collectionSectionUrl;
export enum CollectionListUrlFiltersEnum {
status = "status",
query = "query"
query = "query",
channel = "channel"
}
export type CollectionListUrlFilters = Filters<CollectionListUrlFiltersEnum>;
export type CollectionListUrlDialog = "remove" | TabActionDialog;

View file

@ -17,10 +17,12 @@ import { commonMessages } from "@saleor/intl";
import { maybe } from "@saleor/misc";
import { ListViews } from "@saleor/types";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
import createSortHandler from "@saleor/utils/handlers/sortHandler";
import { mapEdgesToItems } from "@saleor/utils/maps";
import { mapEdgesToItems, mapNodeToChoice } from "@saleor/utils/maps";
import { getSortParams } from "@saleor/utils/sort";
import React from "react";
import { useEffect } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import CollectionListPage from "../../components/CollectionListPage/CollectionListPage";
@ -37,11 +39,13 @@ import {
areFiltersApplied,
deleteFilterTab,
getActiveFilters,
getFilterOpts,
getFilterQueryParam,
getFilterTabs,
getFilterVariables,
saveFilterTab
} from "./filters";
import { getSortQueryVariables } from "./sort";
import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort";
interface CollectionListProps {
params: CollectionListUrlQueryParams;
@ -59,12 +63,33 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
);
const intl = useIntl();
const [
changeFilters,
resetFilters,
handleSearchChange
] = createFilterHandlers({
cleanupFn: reset,
createUrl: collectionListUrl,
getFilterQueryParam,
navigate,
params
});
const { availableChannels } = useAppChannel(false);
const channelOpts = availableChannels
? mapNodeToChoice(availableChannels, channel => channel.slug)
: null;
const selectedChannel = availableChannels.find(
channel => channel.slug === params.channel
);
const paginationState = createPaginationState(settings.rowNumber, params);
const queryVariables = React.useMemo(
() => ({
...paginationState,
filter: getFilterVariables(params),
sort: getSortQueryVariables(params)
sort: getSortQueryVariables(params),
channel: selectedChannel?.slug
}),
[params]
);
@ -90,10 +115,21 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
}
});
const { availableChannels, channel } = useAppChannel();
const filterOpts = getFilterOpts(params, channelOpts);
const tabs = getFilterTabs();
useEffect(() => {
if (!canBeSorted(params.sort, !!selectedChannel)) {
navigate(
collectionListUrl({
...params,
sort: DEFAULT_SORT_KEY
})
);
}
}, [params]);
const currentTab =
params.activeTab === undefined
? areFiltersApplied(params)
@ -101,16 +137,6 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
: 0
: parseInt(params.activeTab, 0);
const handleSearchChange = (query: string) => {
navigate(
collectionListUrl({
...getActiveFilters(params),
activeTab: undefined,
query
})
);
};
const [openModal, closeModal] = createDialogActionHandlers<
CollectionListUrlDialog,
CollectionListUrlQueryParams
@ -152,7 +178,7 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
initialSearch={params.query || ""}
onSearchChange={handleSearchChange}
onAdd={() => navigate(collectionAddUrl())}
onAll={() => navigate(collectionListUrl())}
onAll={resetFilters}
onTabChange={handleTabChange}
onTabDelete={() => openModal("delete-search")}
onTabSave={() => openModal("save-search")}
@ -184,7 +210,9 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
toggle={toggle}
toggleAll={toggleAll}
channelsCount={availableChannels?.length}
selectedChannelId={channel?.id}
selectedChannelId={selectedChannel?.id}
filterOpts={filterOpts}
onFilterChange={changeFilters}
/>
<ActionDialog
open={params.action === "remove" && maybe(() => params.ids.length > 0)}

View file

@ -34,7 +34,8 @@ describe("Filtering URL params", () => {
status: {
active: false,
value: CollectionPublished.PUBLISHED
}
},
channel: undefined
});
it("should be empty if no active filters", () => {
@ -51,7 +52,8 @@ describe("Filtering URL params", () => {
status: {
active: true,
value: CollectionPublished.PUBLISHED
}
},
channel: undefined
});
const filterQueryParams = getFilterQueryParams(

View file

@ -3,6 +3,7 @@ import {
CollectionListFilterOpts
} from "@saleor/collections/components/CollectionListPage";
import { IFilterElement } from "@saleor/components/Filter";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import { findValueInEnum, maybe } from "@saleor/misc";
import {
CollectionFilterInput,
@ -12,7 +13,8 @@ import {
import {
createFilterTabUtils,
createFilterUtils,
getSingleEnumValueQueryParam
getSingleEnumValueQueryParam,
getSingleValueQueryParam
} from "../../../utils/filters";
import {
CollectionListUrlFilters,
@ -23,9 +25,15 @@ import {
export const COLLECTION_FILTERS_KEY = "collectionFilters";
export function getFilterOpts(
params: CollectionListUrlFilters
params: CollectionListUrlFilters,
channels: SingleAutocompleteChoiceType[]
): CollectionListFilterOpts {
return {
channel: {
active: params?.channel !== undefined,
choices: channels,
value: params?.channel
},
status: {
active: maybe(() => params.status !== undefined, false),
value: maybe(() => findValueInEnum(status, CollectionPublished))
@ -56,6 +64,11 @@ export function getFilterQueryParam(
CollectionListUrlFiltersEnum.status,
CollectionPublished
);
case CollectionFilterKeys.channel:
return getSingleValueQueryParam(
filter,
CollectionListUrlFiltersEnum.channel
);
}
}

View file

@ -2,6 +2,23 @@ import { CollectionListUrlSortField } from "@saleor/collections/urls";
import { CollectionSortField } from "@saleor/types/globalTypes";
import { createGetSortQueryVariables } from "@saleor/utils/sort";
export const DEFAULT_SORT_KEY = CollectionListUrlSortField.name;
export function canBeSorted(
sort: CollectionListUrlSortField,
isChannelSelected: boolean
) {
switch (sort) {
case CollectionListUrlSortField.name:
case CollectionListUrlSortField.productCount:
return true;
case CollectionListUrlSortField.available:
return isChannelSelected;
default:
return false;
}
}
export function getSortQueryField(
sort: CollectionListUrlSortField
): CollectionSortField {

View file

@ -3,9 +3,7 @@ import useDateLocalize from "@saleor/hooks/useDateLocalize";
import React from "react";
import { useIntl } from "react-intl";
import { messages } from "./messages";
export const ProductAvailabilityStatusLabel = ({ channel }) => {
export const AvailabilityStatusLabel = ({ channel, messages }) => {
const intl = useIntl();
const localizeDate = useDateLocalize();
@ -32,4 +30,4 @@ export const ProductAvailabilityStatusLabel = ({ channel }) => {
);
};
export default ProductAvailabilityStatusLabel;
export default AvailabilityStatusLabel;

View file

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

View file

@ -74,6 +74,7 @@ const FilterAutocompleteField: React.FC<FilterAutocompleteFieldProps> = ({
payload: {
name: filterField.name,
update: {
active: true,
value: toggle(option.value, filterField.value, (a, b) => a === b)
}
},

View file

@ -165,13 +165,14 @@ const FilterContent: React.FC<FilterContentProps> = ({
filter: IFilterElement<string>
) {
const switchToActive = action.payload.update.active;
if (switchToActive && filter.name !== openedFilter?.name) {
handleFilterAttributeFocus(filter);
} else if (!switchToActive && filter.name === openedFilter?.name) {
handleFilterAttributeFocus(undefined);
}
if (!switchToActive) {
action.payload.update.value = [];
}
onFilterPropertyChange(action);
};
@ -179,7 +180,6 @@ const FilterContent: React.FC<FilterContentProps> = ({
action: FilterReducerAction<T>
) {
const { update } = action.payload;
onFilterPropertyChange({
...action,
payload: { ...action.payload, update: { ...update, active: true } }

View file

@ -32,6 +32,7 @@ const FilterOptionField: React.FC<FilterBaseFieldProps> = ({
payload: {
name: filterField.name,
update: {
active: true,
value: filterField.multiple
? toggle(value, filterField.value, (a, b) => a === b)
: [value]

View file

@ -307,7 +307,10 @@ export const searchPageProps: SearchPageProps = {
export const filterPageProps: FilterPageProps<string, {}> = {
...searchPageProps,
...tabPageProps,
filterOpts: {},
filterOpts: {
status: { value: undefined, active: false },
channel: { value: undefined, active: false }
},
onFilterChange: () => undefined
};

View file

@ -11,6 +11,9 @@ export const commonMessages = defineMessages({
defaultMessage: "Choose file",
description: "button"
},
channel: {
defaultMessage: "Channel"
},
customApps: {
defaultMessage: "Local Apps"
},

View file

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

View file

@ -5,6 +5,7 @@ import {
TableRow,
Typography
} from "@material-ui/core";
import AvailabilityStatusLabel from "@saleor/components/AvailabilityStatusLabel";
import { ChannelsAvailabilityDropdown } from "@saleor/components/ChannelsAvailabilityDropdown";
import Checkbox from "@saleor/components/Checkbox";
import MoneyRange from "@saleor/components/MoneyRange";
@ -35,7 +36,7 @@ import classNames from "classnames";
import React from "react";
import { FormattedMessage } from "react-intl";
import ProductAvailabilityStatusLabel from "../ProductAvailabilityStatusLabel";
import { messages } from "./messages";
const useStyles = makeStyles(
theme => ({
@ -376,7 +377,10 @@ export const ProductList: React.FC<ProductListProps> = props => {
{(!product && <Skeleton />) ||
(!product?.channelListings?.length && "-") ||
(product?.channelListings !== undefined && channel ? (
<ProductAvailabilityStatusLabel channel={channel} />
<AvailabilityStatusLabel
channel={channel}
messages={messages}
/>
) : (
<ChannelsAvailabilityDropdown
allChannelsCount={channelsCount}

View file

@ -5,11 +5,14 @@ import React from "react";
import CollectionListPage, {
CollectionListPageProps
} from "../../../collections/components/CollectionListPage";
import { collections } from "../../../collections/fixtures";
import {
collectionListFilterOpts,
collections
} from "../../../collections/fixtures";
import {
filterPageProps,
listActionsProps,
pageListProps,
searchPageProps,
sortPageProps,
tabPageProps
} from "../../../fixtures";
@ -18,7 +21,7 @@ import Decorator from "../../Decorator";
const props: CollectionListPageProps = {
...listActionsProps,
...pageListProps.default,
...searchPageProps,
...filterPageProps,
...sortPageProps,
channelsCount: 2,
sort: {
@ -27,7 +30,8 @@ const props: CollectionListPageProps = {
},
...tabPageProps,
collections,
selectedChannelId: "123"
selectedChannelId: "123",
filterOpts: collectionListFilterOpts
};
storiesOf("Views / Collections / Collection list", module)