diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 37e6ee9e5..a1c34d1e1 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -3717,6 +3717,62 @@ "context": "GiftCardUpdateDetailsCard title", "string": "Details" }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_balanceAmount": { + "context": "Filter balance amount error", + "string": "Balance amount is missing" + }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_balanceAmountLabel": { + "context": "amount filter label", + "string": "Amount" + }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_balanceCurrency": { + "context": "Filter balance currency error", + "string": "Balance currency is missing" + }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_currencyLabel": { + "context": "currency filter label", + "string": "Currency" + }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_currentBalanceLabel": { + "context": "current balance filter label", + "string": "Current balance" + }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_defaultTabLabel": { + "context": "gift card default tab label", + "string": "All Gift Cards" + }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_disabledOptionLabel": { + "context": "disabled status option label", + "string": "Disabled" + }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_enabledOptionLabel": { + "context": "enabled status option label", + "string": "Enabled" + }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_initialBalanceLabel": { + "context": "initial balance filter label", + "string": "Initial balance" + }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_productLabel": { + "context": "product filter label", + "string": "Product" + }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_searchPlaceholder": { + "context": "gift card search placeholder", + "string": "Search Gift Cards, e.g {exampleGiftCardCode}" + }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_statusLabel": { + "context": "status filter label", + "string": "Status" + }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_tagLabel": { + "context": "tag filter label", + "string": "Tags" + }, + "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardListSearchAndFilters_dot_usedByLabel": { + "context": "used by filter label", + "string": "Used by" + }, "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardsListTable_dot_GiftCardsListTableHeader_dot_disableLabel": { "context": "GiftCardEnableDisableSection enable label", "string": "Deactivate" diff --git a/schema.graphql b/schema.graphql index 37efc9971..232dfa87f 100644 --- a/schema.graphql +++ b/schema.graphql @@ -2428,6 +2428,7 @@ input GiftCardFilterInput { currency: String currentBalance: PriceRangeInput initialBalance: PriceRangeInput + code: String } type GiftCardResend { @@ -5863,6 +5864,7 @@ type Query { menuItems(channel: String, sortBy: MenuItemSortingInput, filter: MenuItemFilterInput, before: String, after: String, first: Int, last: Int): MenuItemCountableConnection giftCard(id: ID!): GiftCard giftCards(sortBy: GiftCardSortingInput, filter: GiftCardFilterInput, before: String, after: String, first: Int, last: Int): GiftCardCountableConnection + giftCardCurrencies: [String!]! plugin(id: ID!): Plugin plugins(filter: PluginFilterInput, sortBy: PluginSortingInput, before: String, after: String, first: Int, last: Int): PluginCountableConnection sale(id: ID!, channel: String): Sale diff --git a/src/components/AppLayout/menuStructure.ts b/src/components/AppLayout/menuStructure.ts index 80802447d..209afcb7c 100644 --- a/src/components/AppLayout/menuStructure.ts +++ b/src/components/AppLayout/menuStructure.ts @@ -9,7 +9,7 @@ import translationIcon from "@assets/images/menu-translation-icon.svg"; import { configurationMenuUrl } from "@saleor/configuration"; import { getConfigMenuItemsPermissions } from "@saleor/configuration/utils"; import { User } from "@saleor/fragments/types/User"; -import { giftCardsListUrl } from "@saleor/giftCards/urls"; +import { giftCardListUrl } from "@saleor/giftCards/urls"; import { commonMessages, sectionNames } from "@saleor/intl"; import { SidebarMenuItem } from "@saleor/macaw-ui"; import { IntlShape } from "react-intl"; @@ -66,7 +66,7 @@ function createMenuStructure(intl: IntlShape, user: User): SidebarMenuItem[] { ariaLabel: "giftCards", label: intl.formatMessage(sectionNames.giftCards), id: "giftCards", - url: giftCardsListUrl(), + url: giftCardListUrl(), permissions: [PermissionEnum.MANAGE_GIFT_CARD] } ], diff --git a/src/components/Filter/FilterAutocompleteField.tsx b/src/components/Filter/FilterAutocompleteField.tsx index 02dd5d6be..c69c73e95 100644 --- a/src/components/Filter/FilterAutocompleteField.tsx +++ b/src/components/Filter/FilterAutocompleteField.tsx @@ -69,26 +69,36 @@ const FilterAutocompleteField: React.FC = ({ const displayNoResults = availableOptions.length === 0 && fieldDisplayValues.length === 0; + const getUpdatedFilterValue = (option: MultiAutocompleteChoiceType) => { + if (filterField.multiple) { + return toggle(option.value, filterField.value, (a, b) => a === b); + } + + return [option.value]; + }; + const handleChange = (option: MultiAutocompleteChoiceType) => { onFilterPropertyChange({ payload: { name: filterField.name, update: { active: true, - value: toggle(option.value, filterField.value, (a, b) => a === b) + value: getUpdatedFilterValue(option) } }, type: "set-property" }); - setDisplayValues({ - ...displayValues, - [filterField.name]: toggle( - option, - fieldDisplayValues, - (a, b) => a.value === b.value - ) - }); + if (filterField.multiple) { + setDisplayValues({ + ...displayValues, + [filterField.name]: toggle( + option, + fieldDisplayValues, + (a, b) => a.value === b.value + ) + }); + } }; const isValueChecked = (displayValue: MultiAutocompleteChoiceType) => @@ -106,18 +116,20 @@ const FilterAutocompleteField: React.FC = ({ return (
- filterField.onSearchChange(event.target.value)} - /> + {filterField?.onSearchChange && ( + filterField.onSearchChange(event.target.value)} + /> + )} {filteredValuesChecked.map(displayValue => (
= ({ if (filterField.multipleFields) { return filterField.multipleFields.reduce( getAutocompleteValuesWithNewValues, - {} + acc ); } @@ -227,7 +227,7 @@ const FilterContent: React.FC = ({ } /> - {currentFilter.active && ( + {currentFilter?.active && ( = ({ />
))} - {filter.type === FieldType.autocomplete && filter.multiple && ( + {filter.type === FieldType.autocomplete && ( ({ return !!compact(value).length; }; +export const isNumberFilterFieldValid = function({ + value +}: IFilterElement) { + const [min, max] = value; + + if (!min && !max) { + return false; + } + + return true; +}; + export const isFilterFieldValid = function( filter: IFilterElement ) { const { type } = filter; switch (type) { + case FieldType.number: + return isNumberFilterFieldValid(filter); case FieldType.boolean: case FieldType.autocomplete: return isAutocompleteFilterFieldValid(filter); diff --git a/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/GiftCardListSearchAndFilters.tsx b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/GiftCardListSearchAndFilters.tsx new file mode 100644 index 000000000..885b28222 --- /dev/null +++ b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/GiftCardListSearchAndFilters.tsx @@ -0,0 +1,185 @@ +import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; +import FilterBar from "@saleor/components/FilterBar"; +import SaveFilterTabDialog, { + SaveFilterTabDialogFormData +} from "@saleor/components/SaveFilterTabDialog"; +import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config"; +import useGiftCardTagsSearch from "@saleor/giftCards/components/GiftCardTagInput/useGiftCardTagsSearch"; +import { giftCardListUrl } from "@saleor/giftCards/urls"; +import { getSearchFetchMoreProps } from "@saleor/hooks/makeTopLevelSearch/utils"; +import useNavigator from "@saleor/hooks/useNavigator"; +import { maybe } from "@saleor/misc"; +import useCustomerSearch from "@saleor/searches/useCustomerSearch"; +import useProductSearch from "@saleor/searches/useProductSearch"; +import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; +import { mapEdgesToItems } from "@saleor/utils/maps"; +import compact from "lodash/compact"; +import React from "react"; +import { useIntl } from "react-intl"; + +import useGiftCardListDialogs from "../providers/GiftCardListDialogsProvider/hooks/useGiftCardListDialogs"; +import useGiftCardList from "../providers/GiftCardListProvider/hooks/useGiftCardList"; +import useGiftCardListBulkActions from "../providers/GiftCardListProvider/hooks/useGiftCardListBulkActions"; +import { GiftCardListActionParamsEnum } from "../types"; +import { + createFilterStructure, + deleteFilterTab, + getActiveFilters, + getFilterOpts, + getFilterQueryParam, + getFiltersCurrentTab, + getFilterTabs, + saveFilterTab +} from "./filters"; +import { giftCardListFilterErrorMessages as errorMessages } from "./messages"; +import { giftCardListSearchAndFiltersMessages as messages } from "./messages"; +import { useGiftCardCurrenciesQuery } from "./queries"; + +const GiftCardListSearchAndFilters: React.FC = () => { + const navigate = useNavigator(); + const intl = useIntl(); + + const { params } = useGiftCardList(); + const { reset } = useGiftCardListBulkActions(); + + const { + closeDialog, + openSearchDeleteDialog, + openSearchSaveDialog + } = useGiftCardListDialogs(); + + const defaultSearchVariables = { + variables: { ...DEFAULT_INITIAL_SEARCH_DATA, first: 5 } + }; + + const { + loadMore: fetchMoreCustomers, + search: searchCustomers, + result: searchCustomersResult + } = useCustomerSearch(defaultSearchVariables); + + const { + loadMore: fetchMoreProducts, + search: searchProducts, + result: searchProductsResult + } = useProductSearch(defaultSearchVariables); + + const { + loadMore: fetchMoreGiftCardTags, + search: searchGiftCardTags, + result: searchGiftCardTagsResult + } = useGiftCardTagsSearch(defaultSearchVariables); + + const { + data: giftCardCurrenciesData, + loading: loadingGiftCardCurrencies + } = useGiftCardCurrenciesQuery(); + + const filterOpts = getFilterOpts({ + params, + productSearchProps: { + ...getSearchFetchMoreProps(searchProductsResult, fetchMoreProducts), + onSearchChange: searchProducts + }, + products: mapEdgesToItems(searchProductsResult?.data?.search), + currencies: giftCardCurrenciesData?.giftCardCurrencies, + loadingCurrencies: loadingGiftCardCurrencies, + customerSearchProps: { + ...getSearchFetchMoreProps(searchCustomersResult, fetchMoreCustomers), + onSearchChange: searchCustomers + }, + customers: mapEdgesToItems(searchCustomersResult?.data?.search), + tagSearchProps: { + ...getSearchFetchMoreProps( + searchGiftCardTagsResult, + fetchMoreGiftCardTags + ), + onSearchChange: searchGiftCardTags + }, + tags: compact( + mapEdgesToItems(searchGiftCardTagsResult?.data?.search)?.map( + ({ tag }) => tag + ) + ) + }); + + const filterStructure = createFilterStructure(intl, filterOpts); + + const tabs = getFilterTabs(); + const currentTab = getFiltersCurrentTab(params, tabs); + + const [ + changeFilters, + resetFilters, + handleSearchChange + ] = createFilterHandlers({ + createUrl: giftCardListUrl, + getFilterQueryParam, + navigate, + params, + cleanupFn: reset + }); + + const handleTabChange = (tab: number) => { + reset(); + navigate( + giftCardListUrl({ + activeTab: tab.toString(), + ...getFilterTabs()[tab - 1].data + }) + ); + }; + + const handleTabDelete = () => { + deleteFilterTab(currentTab); + reset(); + navigate(giftCardListUrl()); + }; + + const handleTabSave = (data: SaveFilterTabDialogFormData) => { + saveFilterTab(data.name, getActiveFilters(params)); + handleTabChange(tabs.length + 1); + }; + + return ( + <> + tab.name)} + currentTab={currentTab} + filterStructure={filterStructure} + initialSearch={params?.query || ""} + onAll={resetFilters} + onFilterChange={changeFilters} + onSearchChange={handleSearchChange} + onTabChange={handleTabChange} + onTabDelete={openSearchDeleteDialog} + onTabSave={openSearchSaveDialog} + searchPlaceholder={intl.formatMessage(messages.searchPlaceholder, { + exampleGiftCardCode: "21F1-39DY-V4U2" + })} + allTabLabel={intl.formatMessage(messages.defaultTabLabel)} + /> + + tabs[currentTab - 1].name, "...")} + /> + + ); +}; + +export default GiftCardListSearchAndFilters; diff --git a/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/filters.ts b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/filters.ts new file mode 100644 index 000000000..079df0120 --- /dev/null +++ b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/filters.ts @@ -0,0 +1,387 @@ +import { IFilter, IFilterElement } from "@saleor/components/Filter"; +import { SearchCustomers_search_edges_node } from "@saleor/searches/types/SearchCustomers"; +import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts"; +import { GiftCardFilterInput } from "@saleor/types/globalTypes"; +import { + createFilterTabUtils, + createFilterUtils, + dedupeFilter, + getMinMaxQueryParam, + getMultipleValueQueryParam, + getSingleValueQueryParam +} from "@saleor/utils/filters"; +import { + createAutocompleteField, + createNumberField, + createOptionsField +} from "@saleor/utils/filters/fields"; +import { + mapNodeToChoice, + mapPersonNodeToChoice, + mapSingleValueNodeToChoice +} from "@saleor/utils/maps"; +import { defineMessages, IntlShape } from "react-intl"; + +import { GiftCardListUrlQueryParams } from "../types"; +import { + GiftCardListFilterKeys, + GiftCardListFilterOpts, + GiftCardListUrlFilters, + GiftCardListUrlFiltersEnum, + GiftCardStatusFilterEnum, + SearchWithFetchMoreProps +} from "./types"; + +export const GIFT_CARD_FILTERS_KEY = "giftCardFilters"; + +interface GiftCardFilterOptsProps { + params: GiftCardListUrlFilters; + currencies: string[]; + loadingCurrencies: boolean; + products: SearchProducts_search_edges_node[]; + productSearchProps: SearchWithFetchMoreProps; + customers: SearchCustomers_search_edges_node[]; + customerSearchProps: SearchWithFetchMoreProps; + tags: string[]; + tagSearchProps: SearchWithFetchMoreProps; +} + +export const getFilterOpts = ({ + params, + currencies, + loadingCurrencies, + products, + productSearchProps, + customers, + customerSearchProps, + tags, + tagSearchProps +}: GiftCardFilterOptsProps): GiftCardListFilterOpts => ({ + currency: { + active: !!params?.currency, + value: params?.currency, + choices: mapSingleValueNodeToChoice(currencies), + displayValues: mapSingleValueNodeToChoice(currencies), + initialSearch: "", + loading: loadingCurrencies + }, + product: { + active: !!params?.product, + value: params?.product, + choices: mapNodeToChoice(products), + displayValues: mapSingleValueNodeToChoice(products), + initialSearch: "", + hasMore: productSearchProps.hasMore, + loading: productSearchProps.loading, + onFetchMore: productSearchProps.onFetchMore, + onSearchChange: productSearchProps.onSearchChange + }, + usedBy: { + active: !!params?.usedBy, + value: params?.usedBy, + choices: mapPersonNodeToChoice(customers), + displayValues: mapPersonNodeToChoice(customers), + initialSearch: "", + hasMore: customerSearchProps.hasMore, + loading: customerSearchProps.loading, + onFetchMore: customerSearchProps.onFetchMore, + onSearchChange: customerSearchProps.onSearchChange + }, + tag: { + active: !!params?.tag, + value: dedupeFilter(params?.tag || []), + choices: mapSingleValueNodeToChoice(tags), + displayValues: mapSingleValueNodeToChoice(tags), + initialSearch: "", + hasMore: tagSearchProps.hasMore, + loading: tagSearchProps.loading, + onFetchMore: tagSearchProps.onFetchMore, + onSearchChange: tagSearchProps.onSearchChange + }, + initialBalanceAmount: { + active: + [params.initialBalanceAmountFrom, params.initialBalanceAmountTo].some( + field => field !== undefined + ) || false, + value: { + max: params.initialBalanceAmountTo || "", + min: params.initialBalanceAmountFrom || "" + } + }, + currentBalanceAmount: { + active: + [params.currentBalanceAmountFrom, params.currentBalanceAmountTo].some( + field => field !== undefined + ) || false, + value: { + max: params.currentBalanceAmountTo || "", + min: params.currentBalanceAmountFrom || "" + } + }, + status: { + active: !!params?.status, + value: params?.status + } +}); + +export function getFilterQueryParam( + filter: IFilterElement +): GiftCardListUrlFilters { + const { name } = filter; + + const { + initialBalanceAmount, + currentBalanceAmount, + tag, + currency, + usedBy, + product, + status + } = GiftCardListFilterKeys; + + switch (name) { + case currency: + case status: + return getSingleValueQueryParam(filter, name); + + case tag: + case product: + case usedBy: + return getMultipleValueQueryParam(filter, name); + + case initialBalanceAmount: + return getMinMaxQueryParam( + filter, + GiftCardListUrlFiltersEnum.initialBalanceAmountFrom, + GiftCardListUrlFiltersEnum.initialBalanceAmountTo + ); + + case currentBalanceAmount: + return getMinMaxQueryParam( + filter, + GiftCardListUrlFiltersEnum.currentBalanceAmountFrom, + GiftCardListUrlFiltersEnum.currentBalanceAmountTo + ); + } +} + +const messages = defineMessages({ + balanceAmountLabel: { + defaultMessage: "Amount", + description: "amount filter label" + }, + tagLabel: { + defaultMessage: "Tags", + description: "tag filter label" + }, + currencyLabel: { + defaultMessage: "Currency", + description: "currency filter label" + }, + productLabel: { + defaultMessage: "Product", + description: "product filter label" + }, + usedByLabel: { + defaultMessage: "Used by", + description: "used by filter label" + }, + statusLabel: { + defaultMessage: "Status", + description: "status filter label" + }, + enabledOptionLabel: { + defaultMessage: "Enabled", + description: "enabled status option label" + }, + disabledOptionLabel: { + defaultMessage: "Disabled", + description: "disabled status option label" + }, + initialBalanceLabel: { + defaultMessage: "Initial balance", + description: "initial balance filter label" + }, + currentBalanceLabel: { + defaultMessage: "Current balance", + description: "current balance filter label" + } +}); + +export function createFilterStructure( + intl: IntlShape, + opts: GiftCardListFilterOpts +): IFilter { + return [ + { + ...createNumberField( + GiftCardListFilterKeys.initialBalanceAmount, + intl.formatMessage(messages.initialBalanceLabel), + opts.initialBalanceAmount.value + ), + multiple: + opts?.initialBalanceAmount?.value?.min !== + opts?.initialBalanceAmount?.value?.max, + active: opts.initialBalanceAmount.active + }, + + { + ...createNumberField( + GiftCardListFilterKeys.currentBalanceAmount, + intl.formatMessage(messages.currentBalanceLabel), + opts.currentBalanceAmount.value + ), + multiple: + opts?.currentBalanceAmount?.value?.min !== + opts?.currentBalanceAmount?.value?.max, + active: opts.currentBalanceAmount.active + }, + { + ...createAutocompleteField( + GiftCardListFilterKeys.currency, + intl.formatMessage(messages.currencyLabel), + [opts.currency.value], + opts.currency.displayValues, + false, + opts.currency.choices, + { + hasMore: opts.currency.hasMore, + initialSearch: "", + loading: opts.currency.loading, + onFetchMore: opts.currency.onFetchMore, + onSearchChange: opts.currency.onSearchChange + } + ), + active: opts.currency.active + }, + { + ...createAutocompleteField( + GiftCardListFilterKeys.tag, + intl.formatMessage(messages.tagLabel), + opts.tag.value, + opts.tag.displayValues, + true, + opts.tag.choices, + { + hasMore: opts.tag.hasMore, + initialSearch: "", + loading: opts.tag.loading, + onFetchMore: opts.tag.onFetchMore, + onSearchChange: opts.tag.onSearchChange + } + ), + active: opts.tag.active + }, + { + ...createAutocompleteField( + GiftCardListFilterKeys.product, + intl.formatMessage(messages.productLabel), + opts.product.value, + opts.product.displayValues, + true, + opts.product.choices, + { + hasMore: opts.product.hasMore, + initialSearch: "", + loading: opts.product.loading, + onFetchMore: opts.product.onFetchMore, + onSearchChange: opts.product.onSearchChange + } + ), + active: opts.product.active + }, + { + ...createAutocompleteField( + GiftCardListFilterKeys.usedBy, + intl.formatMessage(messages.usedByLabel), + opts.usedBy.value, + opts.usedBy.displayValues, + true, + opts.usedBy.choices, + { + hasMore: opts.usedBy.hasMore, + initialSearch: "", + loading: opts.usedBy.loading, + onFetchMore: opts.usedBy.onFetchMore, + onSearchChange: opts.usedBy.onSearchChange + } + ), + active: opts.usedBy.active + }, + { + ...createOptionsField( + GiftCardListFilterKeys.status, + intl.formatMessage(messages.statusLabel), + [opts.status.value], + false, + [ + { + label: intl.formatMessage(messages.enabledOptionLabel), + value: GiftCardStatusFilterEnum.enabled + }, + { + label: intl.formatMessage(messages.disabledOptionLabel), + value: GiftCardStatusFilterEnum.disabled + } + ] + ), + active: opts.status.active + } + ]; +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(GIFT_CARD_FILTERS_KEY); + +export const { + areFiltersApplied, + getActiveFilters, + getFiltersCurrentTab +} = createFilterUtils( + GiftCardListUrlFiltersEnum +); + +export function getFilterVariables({ + status, + tag, + usedBy, + product, + currency, + currentBalanceAmountTo, + currentBalanceAmountFrom, + initialBalanceAmountTo, + initialBalanceAmountFrom, + query +}: GiftCardListUrlQueryParams): GiftCardFilterInput { + const balanceData = currency + ? { + currentBalance: + currentBalanceAmountFrom && currentBalanceAmountTo + ? { + gte: parseFloat(currentBalanceAmountFrom), + lte: parseFloat(currentBalanceAmountTo) + } + : undefined, + initialBalance: + initialBalanceAmountFrom && initialBalanceAmountTo + ? { + gte: parseFloat(initialBalanceAmountFrom), + lte: parseFloat(initialBalanceAmountTo) + } + : undefined + } + : {}; + + return { + code: query, + isActive: !!status ? status === "enabled" : undefined, + tags: tag, + usedBy, + products: product, + currency, + ...balanceData + }; +} diff --git a/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/index.tsx b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/index.tsx new file mode 100644 index 000000000..ead7c3a2a --- /dev/null +++ b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/index.tsx @@ -0,0 +1,2 @@ +export * from "./GiftCardListSearchAndFilters"; +export { default } from "./GiftCardListSearchAndFilters"; diff --git a/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/messages.ts b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/messages.ts new file mode 100644 index 000000000..dc338b006 --- /dev/null +++ b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/messages.ts @@ -0,0 +1,23 @@ +import { defineMessages } from "react-intl"; + +export const giftCardListFilterErrorMessages = defineMessages({ + balanceAmount: { + defaultMessage: "Balance amount is missing", + description: "Filter balance amount error" + }, + balanceCurrency: { + defaultMessage: "Balance currency is missing", + description: "Filter balance currency error" + } +}); + +export const giftCardListSearchAndFiltersMessages = defineMessages({ + searchPlaceholder: { + defaultMessage: "Search Gift Cards, e.g {exampleGiftCardCode}", + description: "gift card search placeholder" + }, + defaultTabLabel: { + defaultMessage: "All Gift Cards", + description: "gift card default tab label" + } +}); diff --git a/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/queries.ts b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/queries.ts new file mode 100644 index 000000000..e2fb0b347 --- /dev/null +++ b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/queries.ts @@ -0,0 +1,14 @@ +import makeQuery from "@saleor/hooks/makeQuery"; +import gql from "graphql-tag"; + +import { GiftCardCurrencies } from "./types/GiftCardCurrencies"; + +const useGiftCardCurrencies = gql` + query GiftCardCurrencies { + giftCardCurrencies + } +`; + +export const useGiftCardCurrenciesQuery = makeQuery( + useGiftCardCurrencies +); diff --git a/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/types.ts b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/types.ts new file mode 100644 index 000000000..18c2c8ed1 --- /dev/null +++ b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/types.ts @@ -0,0 +1,58 @@ +import { + AutocompleteFilterOpts, + FetchMoreProps, + FilterOpts, + Filters, + FiltersWithMultipleValues, + MinMax, + Search, + SearchProps +} from "@saleor/types"; + +export enum GiftCardListUrlFiltersEnum { + currency = "currency", + initialBalanceAmountFrom = "initialBalanceAmountFrom", + initialBalanceAmountTo = "initialBalanceAmountTo", + currentBalanceAmountFrom = "currentBalanceAmountFrom", + currentBalanceAmountTo = "currentBalanceAmountTo", + status = "status" +} + +export enum GiftCardListUrlFiltersWithMultipleValuesEnum { + tag = "tag", + product = "product", + usedBy = "usedBy" +} + +export enum GiftCardListFilterKeys { + currency = "currency", + balance = "balance", + initialBalance = "initialBalance", + currentBalance = "currentBalance", + initialBalanceAmount = "initialBalanceAmount", + currentBalanceAmount = "currentBalanceAmount", + tag = "tag", + product = "product", + usedBy = "usedBy", + status = "status" +} + +export type GiftCardListUrlFilters = Filters & + FiltersWithMultipleValues; + +export interface GiftCardListFilterOpts { + tag: FilterOpts & AutocompleteFilterOpts; + currency: FilterOpts & AutocompleteFilterOpts; + product: FilterOpts & AutocompleteFilterOpts; + usedBy: FilterOpts & AutocompleteFilterOpts; + initialBalanceAmount: FilterOpts; + currentBalanceAmount: FilterOpts; + status: FilterOpts; +} + +export type SearchWithFetchMoreProps = FetchMoreProps & Search & SearchProps; + +export enum GiftCardStatusFilterEnum { + enabled = "enabled", + disabled = "disabled" +} diff --git a/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/types/GiftCardCurrencies.ts b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/types/GiftCardCurrencies.ts new file mode 100644 index 000000000..6b03ff452 --- /dev/null +++ b/src/giftCards/GiftCardsList/GiftCardListSearchAndFilters/types/GiftCardCurrencies.ts @@ -0,0 +1,12 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: GiftCardCurrencies +// ==================================================== + +export interface GiftCardCurrencies { + giftCardCurrencies: string[]; +} diff --git a/src/giftCards/GiftCardsList/GiftCardsListTable/GiftCardsListTable.tsx b/src/giftCards/GiftCardsList/GiftCardsListTable/GiftCardsListTable.tsx index 721b1b7d4..8927db43a 100644 --- a/src/giftCards/GiftCardsList/GiftCardsListTable/GiftCardsListTable.tsx +++ b/src/giftCards/GiftCardsList/GiftCardsListTable/GiftCardsListTable.tsx @@ -23,6 +23,7 @@ import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { giftCardUpdatePageHeaderMessages as giftCardStatusChipMessages } from "../../GiftCardUpdate/GiftCardUpdatePageHeader/messages"; +import GiftCardListSearchAndFilters from "../GiftCardListSearchAndFilters"; import { giftCardsListTableMessages as messages } from "../messages"; import useGiftCardListDialogs from "../providers/GiftCardListDialogsProvider/hooks/useGiftCardListDialogs"; import useGiftCardList from "../providers/GiftCardListProvider/hooks/useGiftCardList"; @@ -77,6 +78,7 @@ const GiftCardsListTable: React.FC = () => { return ( + diff --git a/src/giftCards/GiftCardsList/providers/GiftCardListDialogsProvider/GiftCardListDialogsProvider.tsx b/src/giftCards/GiftCardsList/providers/GiftCardListDialogsProvider/GiftCardListDialogsProvider.tsx index e58977952..a564a78f1 100644 --- a/src/giftCards/GiftCardsList/providers/GiftCardListDialogsProvider/GiftCardListDialogsProvider.tsx +++ b/src/giftCards/GiftCardsList/providers/GiftCardListDialogsProvider/GiftCardListDialogsProvider.tsx @@ -1,6 +1,6 @@ import GiftCardListPageDeleteDialog from "@saleor/giftCards/components/GiftCardDeleteDialog/GiftCardListPageDeleteDialog"; import GiftCardCreateDialog from "@saleor/giftCards/GiftCardCreateDialog"; -import { giftCardsListUrl } from "@saleor/giftCards/urls"; +import { giftCardListUrl } from "@saleor/giftCards/urls"; import useNavigator from "@saleor/hooks/useNavigator"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import React, { createContext } from "react"; @@ -18,6 +18,8 @@ interface GiftCardListDialogsProviderProps { export interface GiftCardListDialogsConsumerProps { openCreateDialog: () => void; openDeleteDialog: (id?: string | React.MouseEvent) => void; + openSearchSaveDialog: () => void; + openSearchDeleteDialog: () => void; closeDialog: () => void; id: string; } @@ -37,7 +39,7 @@ const GiftCardListDialogsProvider: React.FC = const [openDialog, closeDialog] = createDialogActionHandlers< GiftCardListActionParamsEnum, GiftCardListUrlQueryParams - >(navigate, giftCardsListUrl, params); + >(navigate, giftCardListUrl, params); const openCreateDialog = () => openDialog(GiftCardListActionParamsEnum.CREATE); @@ -55,9 +57,17 @@ const GiftCardListDialogsProvider: React.FC = ); }; + const openSearchDeleteDialog = () => + openDialog(GiftCardListActionParamsEnum.DELETE_SEARCH); + + const openSearchSaveDialog = () => + openDialog(GiftCardListActionParamsEnum.SAVE_SEARCH); + const providerValues: GiftCardListDialogsConsumerProps = { openCreateDialog, openDeleteDialog: handleDeleteDialogOpen, + openSearchSaveDialog, + openSearchDeleteDialog, closeDialog, id }; diff --git a/src/giftCards/GiftCardsList/providers/GiftCardListProvider/GiftCardListProvider.tsx b/src/giftCards/GiftCardsList/providers/GiftCardListProvider/GiftCardListProvider.tsx index 69c5e6c50..282be5430 100644 --- a/src/giftCards/GiftCardsList/providers/GiftCardListProvider/GiftCardListProvider.tsx +++ b/src/giftCards/GiftCardsList/providers/GiftCardListProvider/GiftCardListProvider.tsx @@ -15,6 +15,7 @@ import { ListViews } from "@saleor/types"; import { mapEdgesToItems } from "@saleor/utils/maps"; import React, { createContext } from "react"; +import { getFilterVariables } from "../../GiftCardListSearchAndFilters/filters"; import { useGiftCardListQuery } from "../../queries"; import { GiftCardListColummns, GiftCardListUrlQueryParams } from "../../types"; import { @@ -65,7 +66,8 @@ export const GiftCardsListProvider: React.FC = ({ const queryVariables = React.useMemo( () => ({ - ...paginationState + ...paginationState, + filter: getFilterVariables(params) }), [params] ); diff --git a/src/giftCards/GiftCardsList/queries.ts b/src/giftCards/GiftCardsList/queries.ts index d8cce173a..189d17bf6 100644 --- a/src/giftCards/GiftCardsList/queries.ts +++ b/src/giftCards/GiftCardsList/queries.ts @@ -9,8 +9,20 @@ import { GiftCardProductsCount } from "./types/GiftCardProductsCount"; export const giftCardList = gql` ${fragmentUserBase} ${fragmentMoney} - query GiftCardList($first: Int, $after: String, $last: Int, $before: String) { - giftCards(first: $first, after: $after, before: $before, last: $last) { + query GiftCardList( + $first: Int + $after: String + $last: Int + $before: String + $filter: GiftCardFilterInput + ) { + giftCards( + first: $first + after: $after + before: $before + last: $last + filter: $filter + ) { edges { node { id diff --git a/src/giftCards/GiftCardsList/types.ts b/src/giftCards/GiftCardsList/types.ts index 5127b0651..afa1b0668 100644 --- a/src/giftCards/GiftCardsList/types.ts +++ b/src/giftCards/GiftCardsList/types.ts @@ -1,4 +1,12 @@ -import { Dialog, Pagination, SingleAction } from "@saleor/types"; +import { + ActiveTab, + Dialog, + Pagination, + Search, + SingleAction +} from "@saleor/types"; + +import { GiftCardListUrlFilters } from "./GiftCardListSearchAndFilters/types"; export type GiftCardListColummns = | "giftCardCode" @@ -9,11 +17,16 @@ export type GiftCardListColummns = export enum GiftCardListActionParamsEnum { CREATE = "gift-card-create", - DELETE = "gift-card-delete" + DELETE = "gift-card-delete", + SAVE_SEARCH = "save-search", + DELETE_SEARCH = "delete-search" } export type GiftCardListUrlQueryParams = Pagination & Dialog & - SingleAction; + SingleAction & + GiftCardListUrlFilters & + ActiveTab & + Search; export const GIFT_CARD_LIST_QUERY = "GiftCardList"; diff --git a/src/giftCards/GiftCardsList/types/GiftCardList.ts b/src/giftCards/GiftCardsList/types/GiftCardList.ts index 4d6545846..0f366e089 100644 --- a/src/giftCards/GiftCardsList/types/GiftCardList.ts +++ b/src/giftCards/GiftCardsList/types/GiftCardList.ts @@ -3,6 +3,8 @@ // @generated // This file was automatically generated and should not be edited. +import { GiftCardFilterInput } from "./../../../types/globalTypes"; + // ==================================================== // GraphQL query operation: GiftCardList // ==================================================== @@ -67,4 +69,5 @@ export interface GiftCardListVariables { after?: string | null; last?: number | null; before?: string | null; + filter?: GiftCardFilterInput | null; } diff --git a/src/giftCards/urls.ts b/src/giftCards/urls.ts index c06fbdded..db89b2e7b 100644 --- a/src/giftCards/urls.ts +++ b/src/giftCards/urls.ts @@ -8,7 +8,7 @@ export const giftCardsSectionUrlName = "/gift-cards"; export const giftCardsListPath = `${giftCardsSectionUrlName}/`; -export const giftCardsListUrl = (params?: GiftCardListUrlQueryParams) => +export const giftCardListUrl = (params?: GiftCardListUrlQueryParams) => giftCardsListPath + "?" + stringifyQs(params); export const giftCardUrl = ( diff --git a/src/types.ts b/src/types.ts index ae1c4b8ad..2a244b4fd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -209,8 +209,8 @@ export interface FilterOpts { } export interface AutocompleteFilterOpts - extends FetchMoreProps, - SearchPageProps { + extends Partial, + Partial { choices: MultiAutocompleteChoiceType[]; displayValues: MultiAutocompleteChoiceType[]; } diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index bbc2ec47f..aa05f5cca 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -2209,6 +2209,18 @@ export interface GiftCardCreateInput { note?: string | null; } +export interface GiftCardFilterInput { + isActive?: boolean | null; + tag?: string | null; + tags?: (string | null)[] | null; + products?: (string | null)[] | null; + usedBy?: (string | null)[] | null; + currency?: string | null; + currentBalance?: PriceRangeInput | null; + initialBalance?: PriceRangeInput | null; + code?: string | null; +} + export interface GiftCardResendInput { id: string; email?: string | null; diff --git a/src/utils/filters/fields.ts b/src/utils/filters/fields.ts index c83fc037b..9691808f8 100644 --- a/src/utils/filters/fields.ts +++ b/src/utils/filters/fields.ts @@ -83,7 +83,7 @@ export function createOptionsField( export function createAutocompleteField( name: T, label: string, - defaultValue: string[], + defaultValue: string[] = [], displayValues: MultiAutocompleteChoiceType[], multiple: boolean, options: MultiAutocompleteChoiceType[], diff --git a/src/utils/maps.ts b/src/utils/maps.ts index f9aaeb502..495046ba3 100644 --- a/src/utils/maps.ts +++ b/src/utils/maps.ts @@ -4,6 +4,7 @@ import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { MetadataItem } from "@saleor/fragments/types/MetadataItem"; +import { getFullName } from "@saleor/misc"; import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages"; import { Node, SlugNode, TagNode } from "@saleor/types"; import { MetadataInput } from "@saleor/types/globalTypes"; @@ -92,3 +93,22 @@ export function mapSingleValueNodeToChoice>( return (nodes as T[]).map(node => ({ label: node[key], value: node[key] })); } + +interface Person { + firstName: string; + lastName: string; + id: string; +} + +export function mapPersonNodeToChoice( + nodes: T[] +): SingleAutocompleteChoiceType[] { + if (!nodes) { + return []; + } + + return nodes.map(({ firstName, lastName, id }) => ({ + value: id, + label: getFullName({ firstName, lastName }) + })); +}