diff --git a/src/collections/components/CollectionList/CollectionList.tsx b/src/collections/components/CollectionList/CollectionList.tsx index 062a5946a..f25a2986c 100644 --- a/src/collections/components/CollectionList/CollectionList.tsx +++ b/src/collections/components/CollectionList/CollectionList.tsx @@ -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 = props => { } onClick={() => onSort(CollectionListUrlSortField.available)} className={classes.colAvailability} + disabled={ + !canBeSorted( + CollectionListUrlSortField.available, + !!selectedChannelId + ) + } > = props => { data-test="availability" data-test-availability={!!collection?.channelListings?.length} > - {collection && !collection?.channelListings?.length ? ( - "-" - ) : collection?.channelListings !== undefined ? ( - channel ? ( + {(!collection && ) || + (!collection?.channelListings?.length && "-") || + (collection?.channelListings !== undefined && channel ? ( + + ) : ( - ) : null - ) : ( - - )} + ))} ); diff --git a/src/collections/components/CollectionList/messages.ts b/src/collections/components/CollectionList/messages.ts new file mode 100644 index 000000000..00de0eb87 --- /dev/null +++ b/src/collections/components/CollectionList/messages.ts @@ -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" + } +}); diff --git a/src/collections/components/CollectionListPage/CollectionListPage.tsx b/src/collections/components/CollectionListPage/CollectionListPage.tsx index 6a8a8d0cc..2e4282d18 100644 --- a/src/collections/components/CollectionListPage/CollectionListPage.tsx +++ b/src/collections/components/CollectionListPage/CollectionListPage.tsx @@ -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, TabPageProps, + FilterPageProps, ChannelProps { collections: CollectionList_collections_edges_node[]; channelsCount: number; @@ -42,9 +48,13 @@ const CollectionListPage: React.FC = ({ onTabSave, selectedChannelId, tabs, + filterOpts, + onFilterChange, + onFilterAttributeFocus, ...listProps }) => { const intl = useIntl(); + const filterStructure = createFilterStructure(intl, filterOpts); return ( @@ -63,22 +73,25 @@ const CollectionListPage: React.FC = ({ - ; + channel: FilterOpts & { 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 } ]; } diff --git a/src/collections/fixtures.ts b/src/collections/fixtures.ts index ae223bb0d..4f99198d6 100644 --- a/src/collections/fixtures.ts +++ b/src/collections/fixtures.ts @@ -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", diff --git a/src/collections/queries.ts b/src/collections/queries.ts index ce429bced..231d3ab10 100644 --- a/src/collections/queries.ts +++ b/src/collections/queries.ts @@ -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 { diff --git a/src/collections/urls.ts b/src/collections/urls.ts index 22b78f6e2..e5e855772 100644 --- a/src/collections/urls.ts +++ b/src/collections/urls.ts @@ -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; export type CollectionListUrlDialog = "remove" | TabActionDialog; diff --git a/src/collections/views/CollectionList/CollectionList.tsx b/src/collections/views/CollectionList/CollectionList.tsx index baead7734..dd9c4fcfb 100644 --- a/src/collections/views/CollectionList/CollectionList.tsx +++ b/src/collections/views/CollectionList/CollectionList.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ params }) => { toggle={toggle} toggleAll={toggleAll} channelsCount={availableChannels?.length} - selectedChannelId={channel?.id} + selectedChannelId={selectedChannel?.id} + filterOpts={filterOpts} + onFilterChange={changeFilters} /> params.ids.length > 0)} diff --git a/src/collections/views/CollectionList/filters.test.ts b/src/collections/views/CollectionList/filters.test.ts index 7e0bd32fb..0fb91f91e 100644 --- a/src/collections/views/CollectionList/filters.test.ts +++ b/src/collections/views/CollectionList/filters.test.ts @@ -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( diff --git a/src/collections/views/CollectionList/filters.ts b/src/collections/views/CollectionList/filters.ts index c5d9a4fa1..927b11122 100644 --- a/src/collections/views/CollectionList/filters.ts +++ b/src/collections/views/CollectionList/filters.ts @@ -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 + ); } } diff --git a/src/collections/views/CollectionList/sort.ts b/src/collections/views/CollectionList/sort.ts index 34cf3ed2c..000858f53 100644 --- a/src/collections/views/CollectionList/sort.ts +++ b/src/collections/views/CollectionList/sort.ts @@ -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 { diff --git a/src/products/components/ProductAvailabilityStatusLabel/ProductAvailabilityStatusLabel.tsx b/src/components/AvailabilityStatusLabel/AvailabilityStatusLabel.tsx similarity index 82% rename from src/products/components/ProductAvailabilityStatusLabel/ProductAvailabilityStatusLabel.tsx rename to src/components/AvailabilityStatusLabel/AvailabilityStatusLabel.tsx index cd6051044..0e21070ab 100644 --- a/src/products/components/ProductAvailabilityStatusLabel/ProductAvailabilityStatusLabel.tsx +++ b/src/components/AvailabilityStatusLabel/AvailabilityStatusLabel.tsx @@ -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; diff --git a/src/components/AvailabilityStatusLabel/index.ts b/src/components/AvailabilityStatusLabel/index.ts new file mode 100644 index 000000000..ed07509bf --- /dev/null +++ b/src/components/AvailabilityStatusLabel/index.ts @@ -0,0 +1,2 @@ +export { default } from "./AvailabilityStatusLabel"; +export * from "./AvailabilityStatusLabel"; diff --git a/src/components/Filter/FilterAutocompleteField.tsx b/src/components/Filter/FilterAutocompleteField.tsx index 372a8a163..02dd5d6be 100644 --- a/src/components/Filter/FilterAutocompleteField.tsx +++ b/src/components/Filter/FilterAutocompleteField.tsx @@ -74,6 +74,7 @@ const FilterAutocompleteField: React.FC = ({ payload: { name: filterField.name, update: { + active: true, value: toggle(option.value, filterField.value, (a, b) => a === b) } }, diff --git a/src/components/Filter/FilterContent/FilterContent.tsx b/src/components/Filter/FilterContent/FilterContent.tsx index 7398d4c3f..c65c64de5 100644 --- a/src/components/Filter/FilterContent/FilterContent.tsx +++ b/src/components/Filter/FilterContent/FilterContent.tsx @@ -165,13 +165,14 @@ const FilterContent: React.FC = ({ filter: IFilterElement ) { 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 = ({ action: FilterReducerAction ) { const { update } = action.payload; - onFilterPropertyChange({ ...action, payload: { ...action.payload, update: { ...update, active: true } } diff --git a/src/components/Filter/FilterOptionField.tsx b/src/components/Filter/FilterOptionField.tsx index fd8eeeaa3..71dda7e28 100644 --- a/src/components/Filter/FilterOptionField.tsx +++ b/src/components/Filter/FilterOptionField.tsx @@ -32,6 +32,7 @@ const FilterOptionField: React.FC = ({ payload: { name: filterField.name, update: { + active: true, value: filterField.multiple ? toggle(value, filterField.value, (a, b) => a === b) : [value] diff --git a/src/fixtures.ts b/src/fixtures.ts index cba66421c..28c3539e3 100644 --- a/src/fixtures.ts +++ b/src/fixtures.ts @@ -307,7 +307,10 @@ export const searchPageProps: SearchPageProps = { export const filterPageProps: FilterPageProps = { ...searchPageProps, ...tabPageProps, - filterOpts: {}, + filterOpts: { + status: { value: undefined, active: false }, + channel: { value: undefined, active: false } + }, onFilterChange: () => undefined }; diff --git a/src/intl.ts b/src/intl.ts index 6d750a09f..2ba3d7a59 100644 --- a/src/intl.ts +++ b/src/intl.ts @@ -11,6 +11,9 @@ export const commonMessages = defineMessages({ defaultMessage: "Choose file", description: "button" }, + channel: { + defaultMessage: "Channel" + }, customApps: { defaultMessage: "Local Apps" }, diff --git a/src/products/components/ProductAvailabilityStatusLabel/index.ts b/src/products/components/ProductAvailabilityStatusLabel/index.ts deleted file mode 100644 index 602f172eb..000000000 --- a/src/products/components/ProductAvailabilityStatusLabel/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./ProductAvailabilityStatusLabel"; -export * from "./ProductAvailabilityStatusLabel"; diff --git a/src/products/components/ProductList/ProductList.tsx b/src/products/components/ProductList/ProductList.tsx index d4b419a18..9d8d2798b 100644 --- a/src/products/components/ProductList/ProductList.tsx +++ b/src/products/components/ProductList/ProductList.tsx @@ -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 = props => { {(!product && ) || (!product?.channelListings?.length && "-") || (product?.channelListings !== undefined && channel ? ( - + ) : (