Add filters and search to gift cards list (#1466)

* Fix filters not handling autocomplete values properly

* Add handling single selection to filter autocomplete field

* Change giftCardsListUrl function name to GiftCardListUrl for consistency

* Update schema

* Add gift card currencies query and update types

* Add validating function for filter number fields

* Add util function for mapping person node to select choice, fix types

* Add gift card list filters and search

* Add handling of gift card list search and filters dialogs in dialogs provider

* Add gift card search bar in gift card list

* Update gift card list queries and types, add filters to gift card list provider

* Fix types

* Fix types

* Fix currency filters in gift card list

* Update messages

* Remove unnecessary usages of maybe

* Change gift card balance filters not to be send to api when currency filter not present

* Update messages
This commit is contained in:
Magdalena Markusik 2021-10-13 14:42:20 +03:00 committed by GitHub
parent 22db86ed65
commit 185a48b421
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 877 additions and 38 deletions

View file

@ -3717,6 +3717,62 @@
"context": "GiftCardUpdateDetailsCard title", "context": "GiftCardUpdateDetailsCard title",
"string": "Details" "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": { "src_dot_giftCards_dot_GiftCardsList_dot_GiftCardsListTable_dot_GiftCardsListTableHeader_dot_disableLabel": {
"context": "GiftCardEnableDisableSection enable label", "context": "GiftCardEnableDisableSection enable label",
"string": "Deactivate" "string": "Deactivate"

View file

@ -2428,6 +2428,7 @@ input GiftCardFilterInput {
currency: String currency: String
currentBalance: PriceRangeInput currentBalance: PriceRangeInput
initialBalance: PriceRangeInput initialBalance: PriceRangeInput
code: String
} }
type GiftCardResend { type GiftCardResend {
@ -5863,6 +5864,7 @@ type Query {
menuItems(channel: String, sortBy: MenuItemSortingInput, filter: MenuItemFilterInput, before: String, after: String, first: Int, last: Int): MenuItemCountableConnection menuItems(channel: String, sortBy: MenuItemSortingInput, filter: MenuItemFilterInput, before: String, after: String, first: Int, last: Int): MenuItemCountableConnection
giftCard(id: ID!): GiftCard giftCard(id: ID!): GiftCard
giftCards(sortBy: GiftCardSortingInput, filter: GiftCardFilterInput, before: String, after: String, first: Int, last: Int): GiftCardCountableConnection giftCards(sortBy: GiftCardSortingInput, filter: GiftCardFilterInput, before: String, after: String, first: Int, last: Int): GiftCardCountableConnection
giftCardCurrencies: [String!]!
plugin(id: ID!): Plugin plugin(id: ID!): Plugin
plugins(filter: PluginFilterInput, sortBy: PluginSortingInput, before: String, after: String, first: Int, last: Int): PluginCountableConnection plugins(filter: PluginFilterInput, sortBy: PluginSortingInput, before: String, after: String, first: Int, last: Int): PluginCountableConnection
sale(id: ID!, channel: String): Sale sale(id: ID!, channel: String): Sale

View file

@ -9,7 +9,7 @@ import translationIcon from "@assets/images/menu-translation-icon.svg";
import { configurationMenuUrl } from "@saleor/configuration"; import { configurationMenuUrl } from "@saleor/configuration";
import { getConfigMenuItemsPermissions } from "@saleor/configuration/utils"; import { getConfigMenuItemsPermissions } from "@saleor/configuration/utils";
import { User } from "@saleor/fragments/types/User"; 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 { commonMessages, sectionNames } from "@saleor/intl";
import { SidebarMenuItem } from "@saleor/macaw-ui"; import { SidebarMenuItem } from "@saleor/macaw-ui";
import { IntlShape } from "react-intl"; import { IntlShape } from "react-intl";
@ -66,7 +66,7 @@ function createMenuStructure(intl: IntlShape, user: User): SidebarMenuItem[] {
ariaLabel: "giftCards", ariaLabel: "giftCards",
label: intl.formatMessage(sectionNames.giftCards), label: intl.formatMessage(sectionNames.giftCards),
id: "giftCards", id: "giftCards",
url: giftCardsListUrl(), url: giftCardListUrl(),
permissions: [PermissionEnum.MANAGE_GIFT_CARD] permissions: [PermissionEnum.MANAGE_GIFT_CARD]
} }
], ],

View file

@ -69,26 +69,36 @@ const FilterAutocompleteField: React.FC<FilterAutocompleteFieldProps> = ({
const displayNoResults = const displayNoResults =
availableOptions.length === 0 && fieldDisplayValues.length === 0; 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) => { const handleChange = (option: MultiAutocompleteChoiceType) => {
onFilterPropertyChange({ onFilterPropertyChange({
payload: { payload: {
name: filterField.name, name: filterField.name,
update: { update: {
active: true, active: true,
value: toggle(option.value, filterField.value, (a, b) => a === b) value: getUpdatedFilterValue(option)
} }
}, },
type: "set-property" type: "set-property"
}); });
setDisplayValues({ if (filterField.multiple) {
...displayValues, setDisplayValues({
[filterField.name]: toggle( ...displayValues,
option, [filterField.name]: toggle(
fieldDisplayValues, option,
(a, b) => a.value === b.value fieldDisplayValues,
) (a, b) => a.value === b.value
}); )
});
}
}; };
const isValueChecked = (displayValue: MultiAutocompleteChoiceType) => const isValueChecked = (displayValue: MultiAutocompleteChoiceType) =>
@ -106,18 +116,20 @@ const FilterAutocompleteField: React.FC<FilterAutocompleteFieldProps> = ({
return ( return (
<div {...rest}> <div {...rest}>
<TextField {filterField?.onSearchChange && (
data-test="filterFieldAutocompleteInput" <TextField
className={classes.inputContainer} data-test="filterFieldAutocompleteInput"
fullWidth className={classes.inputContainer}
name={filterField.name + "_autocomplete"} fullWidth
InputProps={{ name={filterField.name + "_autocomplete"}
classes: { InputProps={{
input: classes.input classes: {
} input: classes.input
}} }
onChange={event => filterField.onSearchChange(event.target.value)} }}
/> onChange={event => filterField.onSearchChange(event.target.value)}
/>
)}
{filteredValuesChecked.map(displayValue => ( {filteredValuesChecked.map(displayValue => (
<div className={classes.option} key={displayValue.value}> <div className={classes.option} key={displayValue.value}>
<FormControlLabel <FormControlLabel

View file

@ -119,7 +119,7 @@ const FilterContent: React.FC<FilterContentProps> = ({
if (filterField.multipleFields) { if (filterField.multipleFields) {
return filterField.multipleFields.reduce( return filterField.multipleFields.reduce(
getAutocompleteValuesWithNewValues, getAutocompleteValuesWithNewValues,
{} acc
); );
} }
@ -227,7 +227,7 @@ const FilterContent: React.FC<FilterContentProps> = ({
} }
/> />
</ExpansionPanelSummary> </ExpansionPanelSummary>
{currentFilter.active && ( {currentFilter?.active && (
<FilterErrorsList <FilterErrorsList
errors={errors?.[filter.name]} errors={errors?.[filter.name]}
errorMessages={errorMessages} errorMessages={errorMessages}

View file

@ -156,7 +156,7 @@ const FilterContentBody: React.FC<FilterContentBodyProps> = ({
/> />
</div> </div>
))} ))}
{filter.type === FieldType.autocomplete && filter.multiple && ( {filter.type === FieldType.autocomplete && (
<FilterAutocompleteField <FilterAutocompleteField
data-test={filterTestingContext} data-test={filterTestingContext}
data-test-id={filter.name} data-test-id={filter.name}

View file

@ -16,12 +16,26 @@ export const isAutocompleteFilterFieldValid = function<T extends string>({
return !!compact(value).length; return !!compact(value).length;
}; };
export const isNumberFilterFieldValid = function<T extends string>({
value
}: IFilterElement<T>) {
const [min, max] = value;
if (!min && !max) {
return false;
}
return true;
};
export const isFilterFieldValid = function<T extends string>( export const isFilterFieldValid = function<T extends string>(
filter: IFilterElement<T> filter: IFilterElement<T>
) { ) {
const { type } = filter; const { type } = filter;
switch (type) { switch (type) {
case FieldType.number:
return isNumberFilterFieldValid(filter);
case FieldType.boolean: case FieldType.boolean:
case FieldType.autocomplete: case FieldType.autocomplete:
return isAutocompleteFilterFieldValid(filter); return isAutocompleteFilterFieldValid(filter);

View file

@ -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 (
<>
<FilterBar
errorMessages={{
initialBalanceAmount: errorMessages.balanceAmount,
initialBalanceCurrency: errorMessages.balanceCurrency,
currentBalanceAmount: errorMessages.balanceAmount,
currentBalanceCurrency: errorMessages.balanceCurrency
}}
tabs={tabs.map(tab => 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)}
/>
<SaveFilterTabDialog
open={params.action === GiftCardListActionParamsEnum.SAVE_SEARCH}
confirmButtonState="default"
onClose={closeDialog}
onSubmit={handleTabSave}
/>
<DeleteFilterTabDialog
open={params.action === GiftCardListActionParamsEnum.DELETE_SEARCH}
confirmButtonState="default"
onClose={closeDialog}
onSubmit={handleTabDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
/>
</>
);
};
export default GiftCardListSearchAndFilters;

View file

@ -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<GiftCardListFilterKeys>
): 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<GiftCardListFilterKeys> {
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<GiftCardListUrlFilters>(GIFT_CARD_FILTERS_KEY);
export const {
areFiltersApplied,
getActiveFilters,
getFiltersCurrentTab
} = createFilterUtils<GiftCardListUrlQueryParams, GiftCardListUrlFilters>(
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
};
}

View file

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

View file

@ -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"
}
});

View file

@ -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<GiftCardCurrencies, {}>(
useGiftCardCurrencies
);

View file

@ -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<GiftCardListUrlFiltersEnum> &
FiltersWithMultipleValues<GiftCardListUrlFiltersWithMultipleValuesEnum>;
export interface GiftCardListFilterOpts {
tag: FilterOpts<string[]> & AutocompleteFilterOpts;
currency: FilterOpts<string> & AutocompleteFilterOpts;
product: FilterOpts<string[]> & AutocompleteFilterOpts;
usedBy: FilterOpts<string[]> & AutocompleteFilterOpts;
initialBalanceAmount: FilterOpts<MinMax>;
currentBalanceAmount: FilterOpts<MinMax>;
status: FilterOpts<string>;
}
export type SearchWithFetchMoreProps = FetchMoreProps & Search & SearchProps;
export enum GiftCardStatusFilterEnum {
enabled = "enabled",
disabled = "disabled"
}

View file

@ -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[];
}

View file

@ -23,6 +23,7 @@ import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { giftCardUpdatePageHeaderMessages as giftCardStatusChipMessages } from "../../GiftCardUpdate/GiftCardUpdatePageHeader/messages"; import { giftCardUpdatePageHeaderMessages as giftCardStatusChipMessages } from "../../GiftCardUpdate/GiftCardUpdatePageHeader/messages";
import GiftCardListSearchAndFilters from "../GiftCardListSearchAndFilters";
import { giftCardsListTableMessages as messages } from "../messages"; import { giftCardsListTableMessages as messages } from "../messages";
import useGiftCardListDialogs from "../providers/GiftCardListDialogsProvider/hooks/useGiftCardListDialogs"; import useGiftCardListDialogs from "../providers/GiftCardListDialogsProvider/hooks/useGiftCardListDialogs";
import useGiftCardList from "../providers/GiftCardListProvider/hooks/useGiftCardList"; import useGiftCardList from "../providers/GiftCardListProvider/hooks/useGiftCardList";
@ -77,6 +78,7 @@ const GiftCardsListTable: React.FC = () => {
return ( return (
<Card> <Card>
<GiftCardListSearchAndFilters />
<ResponsiveTable> <ResponsiveTable>
<GiftCardsListTableHeader /> <GiftCardsListTableHeader />
<GiftCardsListTableFooter /> <GiftCardsListTableFooter />

View file

@ -1,6 +1,6 @@
import GiftCardListPageDeleteDialog from "@saleor/giftCards/components/GiftCardDeleteDialog/GiftCardListPageDeleteDialog"; import GiftCardListPageDeleteDialog from "@saleor/giftCards/components/GiftCardDeleteDialog/GiftCardListPageDeleteDialog";
import GiftCardCreateDialog from "@saleor/giftCards/GiftCardCreateDialog"; 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 useNavigator from "@saleor/hooks/useNavigator";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import React, { createContext } from "react"; import React, { createContext } from "react";
@ -18,6 +18,8 @@ interface GiftCardListDialogsProviderProps {
export interface GiftCardListDialogsConsumerProps { export interface GiftCardListDialogsConsumerProps {
openCreateDialog: () => void; openCreateDialog: () => void;
openDeleteDialog: (id?: string | React.MouseEvent) => void; openDeleteDialog: (id?: string | React.MouseEvent) => void;
openSearchSaveDialog: () => void;
openSearchDeleteDialog: () => void;
closeDialog: () => void; closeDialog: () => void;
id: string; id: string;
} }
@ -37,7 +39,7 @@ const GiftCardListDialogsProvider: React.FC<GiftCardListDialogsProviderProps> =
const [openDialog, closeDialog] = createDialogActionHandlers< const [openDialog, closeDialog] = createDialogActionHandlers<
GiftCardListActionParamsEnum, GiftCardListActionParamsEnum,
GiftCardListUrlQueryParams GiftCardListUrlQueryParams
>(navigate, giftCardsListUrl, params); >(navigate, giftCardListUrl, params);
const openCreateDialog = () => const openCreateDialog = () =>
openDialog(GiftCardListActionParamsEnum.CREATE); openDialog(GiftCardListActionParamsEnum.CREATE);
@ -55,9 +57,17 @@ const GiftCardListDialogsProvider: React.FC<GiftCardListDialogsProviderProps> =
); );
}; };
const openSearchDeleteDialog = () =>
openDialog(GiftCardListActionParamsEnum.DELETE_SEARCH);
const openSearchSaveDialog = () =>
openDialog(GiftCardListActionParamsEnum.SAVE_SEARCH);
const providerValues: GiftCardListDialogsConsumerProps = { const providerValues: GiftCardListDialogsConsumerProps = {
openCreateDialog, openCreateDialog,
openDeleteDialog: handleDeleteDialogOpen, openDeleteDialog: handleDeleteDialogOpen,
openSearchSaveDialog,
openSearchDeleteDialog,
closeDialog, closeDialog,
id id
}; };

View file

@ -15,6 +15,7 @@ import { ListViews } from "@saleor/types";
import { mapEdgesToItems } from "@saleor/utils/maps"; import { mapEdgesToItems } from "@saleor/utils/maps";
import React, { createContext } from "react"; import React, { createContext } from "react";
import { getFilterVariables } from "../../GiftCardListSearchAndFilters/filters";
import { useGiftCardListQuery } from "../../queries"; import { useGiftCardListQuery } from "../../queries";
import { GiftCardListColummns, GiftCardListUrlQueryParams } from "../../types"; import { GiftCardListColummns, GiftCardListUrlQueryParams } from "../../types";
import { import {
@ -65,7 +66,8 @@ export const GiftCardsListProvider: React.FC<GiftCardsListProviderProps> = ({
const queryVariables = React.useMemo<GiftCardListVariables>( const queryVariables = React.useMemo<GiftCardListVariables>(
() => ({ () => ({
...paginationState ...paginationState,
filter: getFilterVariables(params)
}), }),
[params] [params]
); );

View file

@ -9,8 +9,20 @@ import { GiftCardProductsCount } from "./types/GiftCardProductsCount";
export const giftCardList = gql` export const giftCardList = gql`
${fragmentUserBase} ${fragmentUserBase}
${fragmentMoney} ${fragmentMoney}
query GiftCardList($first: Int, $after: String, $last: Int, $before: String) { query GiftCardList(
giftCards(first: $first, after: $after, before: $before, last: $last) { $first: Int
$after: String
$last: Int
$before: String
$filter: GiftCardFilterInput
) {
giftCards(
first: $first
after: $after
before: $before
last: $last
filter: $filter
) {
edges { edges {
node { node {
id id

View file

@ -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 = export type GiftCardListColummns =
| "giftCardCode" | "giftCardCode"
@ -9,11 +17,16 @@ export type GiftCardListColummns =
export enum GiftCardListActionParamsEnum { export enum GiftCardListActionParamsEnum {
CREATE = "gift-card-create", CREATE = "gift-card-create",
DELETE = "gift-card-delete" DELETE = "gift-card-delete",
SAVE_SEARCH = "save-search",
DELETE_SEARCH = "delete-search"
} }
export type GiftCardListUrlQueryParams = Pagination & export type GiftCardListUrlQueryParams = Pagination &
Dialog<GiftCardListActionParamsEnum> & Dialog<GiftCardListActionParamsEnum> &
SingleAction; SingleAction &
GiftCardListUrlFilters &
ActiveTab &
Search;
export const GIFT_CARD_LIST_QUERY = "GiftCardList"; export const GIFT_CARD_LIST_QUERY = "GiftCardList";

View file

@ -3,6 +3,8 @@
// @generated // @generated
// This file was automatically generated and should not be edited. // This file was automatically generated and should not be edited.
import { GiftCardFilterInput } from "./../../../types/globalTypes";
// ==================================================== // ====================================================
// GraphQL query operation: GiftCardList // GraphQL query operation: GiftCardList
// ==================================================== // ====================================================
@ -67,4 +69,5 @@ export interface GiftCardListVariables {
after?: string | null; after?: string | null;
last?: number | null; last?: number | null;
before?: string | null; before?: string | null;
filter?: GiftCardFilterInput | null;
} }

View file

@ -8,7 +8,7 @@ export const giftCardsSectionUrlName = "/gift-cards";
export const giftCardsListPath = `${giftCardsSectionUrlName}/`; export const giftCardsListPath = `${giftCardsSectionUrlName}/`;
export const giftCardsListUrl = (params?: GiftCardListUrlQueryParams) => export const giftCardListUrl = (params?: GiftCardListUrlQueryParams) =>
giftCardsListPath + "?" + stringifyQs(params); giftCardsListPath + "?" + stringifyQs(params);
export const giftCardUrl = ( export const giftCardUrl = (

View file

@ -209,8 +209,8 @@ export interface FilterOpts<T> {
} }
export interface AutocompleteFilterOpts export interface AutocompleteFilterOpts
extends FetchMoreProps, extends Partial<FetchMoreProps>,
SearchPageProps { Partial<SearchPageProps> {
choices: MultiAutocompleteChoiceType[]; choices: MultiAutocompleteChoiceType[];
displayValues: MultiAutocompleteChoiceType[]; displayValues: MultiAutocompleteChoiceType[];
} }

View file

@ -2209,6 +2209,18 @@ export interface GiftCardCreateInput {
note?: string | null; 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 { export interface GiftCardResendInput {
id: string; id: string;
email?: string | null; email?: string | null;

View file

@ -83,7 +83,7 @@ export function createOptionsField<T extends string>(
export function createAutocompleteField<T extends string>( export function createAutocompleteField<T extends string>(
name: T, name: T,
label: string, label: string,
defaultValue: string[], defaultValue: string[] = [],
displayValues: MultiAutocompleteChoiceType[], displayValues: MultiAutocompleteChoiceType[],
multiple: boolean, multiple: boolean,
options: MultiAutocompleteChoiceType[], options: MultiAutocompleteChoiceType[],

View file

@ -4,6 +4,7 @@ import {
SingleAutocompleteChoiceType SingleAutocompleteChoiceType
} from "@saleor/components/SingleAutocompleteSelectField"; } from "@saleor/components/SingleAutocompleteSelectField";
import { MetadataItem } from "@saleor/fragments/types/MetadataItem"; import { MetadataItem } from "@saleor/fragments/types/MetadataItem";
import { getFullName } from "@saleor/misc";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages"; import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { Node, SlugNode, TagNode } from "@saleor/types"; import { Node, SlugNode, TagNode } from "@saleor/types";
import { MetadataInput } from "@saleor/types/globalTypes"; import { MetadataInput } from "@saleor/types/globalTypes";
@ -92,3 +93,22 @@ export function mapSingleValueNodeToChoice<T extends Record<string, any>>(
return (nodes as T[]).map(node => ({ label: node[key], value: node[key] })); return (nodes as T[]).map(node => ({ label: node[key], value: node[key] }));
} }
interface Person {
firstName: string;
lastName: string;
id: string;
}
export function mapPersonNodeToChoice<T extends Person>(
nodes: T[]
): SingleAutocompleteChoiceType[] {
if (!nodes) {
return [];
}
return nodes.map(({ firstName, lastName, id }) => ({
value: id,
label: getFullName({ firstName, lastName })
}));
}