Merge branch 'master' into fix/password-reset
This commit is contained in:
commit
95df0e8739
186 changed files with 8767 additions and 7936 deletions
|
@ -26,6 +26,7 @@ All notable, unreleased changes to this project will be documented in this file.
|
||||||
- Stop using deprecated fields - #357 by @dominik-zeglen
|
- Stop using deprecated fields - #357 by @dominik-zeglen
|
||||||
- Throw error when API_URI is not set - #375 by @dominik-zeglen
|
- Throw error when API_URI is not set - #375 by @dominik-zeglen
|
||||||
- Fix variant stock input - #377 by @dominik-zeglen
|
- Fix variant stock input - #377 by @dominik-zeglen
|
||||||
|
- Add filtering to views - #361 by @dominik-zeglen
|
||||||
- Do not render password change if authenticating - #378 by @dominik-zeglen
|
- Do not render password change if authenticating - #378 by @dominik-zeglen
|
||||||
|
|
||||||
## 2.0.0
|
## 2.0.0
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -164,6 +164,7 @@
|
||||||
"@assets(.*)$": "<rootDir>/assets/$1",
|
"@assets(.*)$": "<rootDir>/assets/$1",
|
||||||
"@locale(.*)$": "<rootDir>/locale/$1",
|
"@locale(.*)$": "<rootDir>/locale/$1",
|
||||||
"@saleor(.*)$": "<rootDir>/src/$1",
|
"@saleor(.*)$": "<rootDir>/src/$1",
|
||||||
|
"@test/(.*)$": "<rootDir>/testUtils/$1",
|
||||||
"^lodash-es(.*)$": "lodash/$1"
|
"^lodash-es(.*)$": "lodash/$1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
3
react-intl.d.ts
vendored
3
react-intl.d.ts
vendored
|
@ -1,4 +1,5 @@
|
||||||
declare module "react-intl" {
|
declare module "react-intl" {
|
||||||
|
import { OptionalIntlConfig } from "react-intl/dist/components/provider";
|
||||||
import * as ReactIntl from "node_modules/react-intl";
|
import * as ReactIntl from "node_modules/react-intl";
|
||||||
export * from "node_modules/react-intl";
|
export * from "node_modules/react-intl";
|
||||||
|
|
||||||
|
@ -51,4 +52,6 @@ declare module "react-intl" {
|
||||||
> extends React.Component<FormattedMessageProps<TValues>> {}
|
> extends React.Component<FormattedMessageProps<TValues>> {}
|
||||||
|
|
||||||
export function useIntl(): IntlShape;
|
export function useIntl(): IntlShape;
|
||||||
|
|
||||||
|
export function createIntl(config: OptionalIntlConfig): IntlShape;
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@ input AccountInput {
|
||||||
|
|
||||||
type AccountRegister {
|
type AccountRegister {
|
||||||
errors: [Error!]
|
errors: [Error!]
|
||||||
requiresConfirmation: Boolean!
|
requiresConfirmation: Boolean
|
||||||
accountErrors: [AccountError!]
|
accountErrors: [AccountError!]
|
||||||
user: User
|
user: User
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import React from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import AppHeader from "@saleor/components/AppHeader";
|
import AppHeader from "@saleor/components/AppHeader";
|
||||||
import SearchBar from "@saleor/components/SearchBar";
|
import FilterBar from "@saleor/components/FilterBar";
|
||||||
import { sectionNames } from "@saleor/intl";
|
import { sectionNames } from "@saleor/intl";
|
||||||
import { AttributeListUrlSortField } from "@saleor/attributes/urls";
|
import { AttributeListUrlSortField } from "@saleor/attributes/urls";
|
||||||
import Container from "../../../components/Container";
|
import Container from "../../../components/Container";
|
||||||
|
@ -12,17 +12,22 @@ import PageHeader from "../../../components/PageHeader";
|
||||||
import {
|
import {
|
||||||
ListActions,
|
ListActions,
|
||||||
PageListProps,
|
PageListProps,
|
||||||
SearchPageProps,
|
FilterPageProps,
|
||||||
TabPageProps,
|
TabPageProps,
|
||||||
SortPage
|
SortPage
|
||||||
} from "../../../types";
|
} from "../../../types";
|
||||||
import { AttributeList_attributes_edges_node } from "../../types/AttributeList";
|
import { AttributeList_attributes_edges_node } from "../../types/AttributeList";
|
||||||
import AttributeList from "../AttributeList/AttributeList";
|
import AttributeList from "../AttributeList/AttributeList";
|
||||||
|
import {
|
||||||
|
createFilterStructure,
|
||||||
|
AttributeListFilterOpts,
|
||||||
|
AttributeFilterKeys
|
||||||
|
} from "./filters";
|
||||||
|
|
||||||
export interface AttributeListPageProps
|
export interface AttributeListPageProps
|
||||||
extends PageListProps,
|
extends PageListProps,
|
||||||
ListActions,
|
ListActions,
|
||||||
SearchPageProps,
|
FilterPageProps<AttributeFilterKeys, AttributeListFilterOpts>,
|
||||||
SortPage<AttributeListUrlSortField>,
|
SortPage<AttributeListUrlSortField>,
|
||||||
TabPageProps {
|
TabPageProps {
|
||||||
attributes: AttributeList_attributes_edges_node[];
|
attributes: AttributeList_attributes_edges_node[];
|
||||||
|
@ -30,9 +35,12 @@ export interface AttributeListPageProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttributeListPage: React.FC<AttributeListPageProps> = ({
|
const AttributeListPage: React.FC<AttributeListPageProps> = ({
|
||||||
|
currencySymbol,
|
||||||
|
filterOpts,
|
||||||
|
initialSearch,
|
||||||
onAdd,
|
onAdd,
|
||||||
onBack,
|
onBack,
|
||||||
initialSearch,
|
onFilterChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
currentTab,
|
currentTab,
|
||||||
onAll,
|
onAll,
|
||||||
|
@ -44,6 +52,8 @@ const AttributeListPage: React.FC<AttributeListPageProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const structure = createFilterStructure(intl, filterOpts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<AppHeader onBack={onBack}>
|
<AppHeader onBack={onBack}>
|
||||||
|
@ -58,18 +68,21 @@ const AttributeListPage: React.FC<AttributeListPageProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Card>
|
<Card>
|
||||||
<SearchBar
|
<FilterBar
|
||||||
allTabLabel={intl.formatMessage({
|
allTabLabel={intl.formatMessage({
|
||||||
defaultMessage: "All Attributes",
|
defaultMessage: "All Attributes",
|
||||||
description: "tab name"
|
description: "tab name"
|
||||||
})}
|
})}
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
filterStructure={structure}
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
searchPlaceholder={intl.formatMessage({
|
searchPlaceholder={intl.formatMessage({
|
||||||
defaultMessage: "Search Attribute"
|
defaultMessage: "Search Attribute"
|
||||||
})}
|
})}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
onAll={onAll}
|
onAll={onAll}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
onTabChange={onTabChange}
|
onTabChange={onTabChange}
|
||||||
onTabDelete={onTabDelete}
|
onTabDelete={onTabDelete}
|
||||||
|
|
131
src/attributes/components/AttributeListPage/filters.ts
Normal file
131
src/attributes/components/AttributeListPage/filters.ts
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
import { defineMessages, IntlShape } from "react-intl";
|
||||||
|
|
||||||
|
import { FilterOpts } from "@saleor/types";
|
||||||
|
import { commonMessages } from "@saleor/intl";
|
||||||
|
import { IFilter } from "@saleor/components/Filter";
|
||||||
|
import { createBooleanField } from "@saleor/utils/filters/fields";
|
||||||
|
|
||||||
|
export enum AttributeFilterKeys {
|
||||||
|
availableInGrid = "availableInGrid",
|
||||||
|
filterableInDashboard = "filterableInDashboard",
|
||||||
|
filterableInStorefront = "filterableInStorefront",
|
||||||
|
isVariantOnly = "isVariantOnly",
|
||||||
|
valueRequired = "valueRequired",
|
||||||
|
visibleInStorefront = "visibleInStorefront"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttributeListFilterOpts {
|
||||||
|
availableInGrid: FilterOpts<boolean>;
|
||||||
|
filterableInDashboard: FilterOpts<boolean>;
|
||||||
|
filterableInStorefront: FilterOpts<boolean>;
|
||||||
|
isVariantOnly: FilterOpts<boolean>;
|
||||||
|
valueRequired: FilterOpts<boolean>;
|
||||||
|
visibleInStorefront: FilterOpts<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
availableInGrid: {
|
||||||
|
defaultMessage: "Can be used as column",
|
||||||
|
description: "attribute can be column in product list table"
|
||||||
|
},
|
||||||
|
filterableInDashboard: {
|
||||||
|
defaultMessage: "Filterable in Dashboard",
|
||||||
|
description: "use attribute in filtering"
|
||||||
|
},
|
||||||
|
filterableInStorefront: {
|
||||||
|
defaultMessage: "Filterable in Storefront",
|
||||||
|
description: "use attribute in filtering"
|
||||||
|
},
|
||||||
|
isVariantOnly: {
|
||||||
|
defaultMessage: "Variant Only",
|
||||||
|
description: "attribute can be used only in variants"
|
||||||
|
},
|
||||||
|
valueRequired: {
|
||||||
|
defaultMessage: "Value Required",
|
||||||
|
description: "attribute value is required"
|
||||||
|
},
|
||||||
|
visibleInStorefront: {
|
||||||
|
defaultMessage: "Visible on Product Page in Storefront",
|
||||||
|
description: "attribute"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createFilterStructure(
|
||||||
|
intl: IntlShape,
|
||||||
|
opts: AttributeListFilterOpts
|
||||||
|
): IFilter<AttributeFilterKeys> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...createBooleanField(
|
||||||
|
AttributeFilterKeys.availableInGrid,
|
||||||
|
intl.formatMessage(messages.availableInGrid),
|
||||||
|
opts.availableInGrid.value,
|
||||||
|
{
|
||||||
|
negative: intl.formatMessage(commonMessages.no),
|
||||||
|
positive: intl.formatMessage(commonMessages.yes)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
active: opts.availableInGrid.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createBooleanField(
|
||||||
|
AttributeFilterKeys.filterableInDashboard,
|
||||||
|
intl.formatMessage(messages.filterableInDashboard),
|
||||||
|
opts.filterableInDashboard.value,
|
||||||
|
{
|
||||||
|
negative: intl.formatMessage(commonMessages.no),
|
||||||
|
positive: intl.formatMessage(commonMessages.yes)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
active: opts.filterableInDashboard.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createBooleanField(
|
||||||
|
AttributeFilterKeys.filterableInStorefront,
|
||||||
|
intl.formatMessage(messages.filterableInStorefront),
|
||||||
|
opts.filterableInStorefront.value,
|
||||||
|
{
|
||||||
|
negative: intl.formatMessage(commonMessages.no),
|
||||||
|
positive: intl.formatMessage(commonMessages.yes)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
active: opts.filterableInStorefront.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createBooleanField(
|
||||||
|
AttributeFilterKeys.isVariantOnly,
|
||||||
|
intl.formatMessage(messages.isVariantOnly),
|
||||||
|
opts.isVariantOnly.value,
|
||||||
|
{
|
||||||
|
negative: intl.formatMessage(commonMessages.no),
|
||||||
|
positive: intl.formatMessage(commonMessages.yes)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
active: opts.isVariantOnly.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createBooleanField(
|
||||||
|
AttributeFilterKeys.valueRequired,
|
||||||
|
intl.formatMessage(messages.valueRequired),
|
||||||
|
opts.valueRequired.value,
|
||||||
|
{
|
||||||
|
negative: intl.formatMessage(commonMessages.no),
|
||||||
|
positive: intl.formatMessage(commonMessages.yes)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
active: opts.valueRequired.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createBooleanField(
|
||||||
|
AttributeFilterKeys.visibleInStorefront,
|
||||||
|
intl.formatMessage(messages.visibleInStorefront),
|
||||||
|
opts.visibleInStorefront.value,
|
||||||
|
{
|
||||||
|
negative: intl.formatMessage(commonMessages.no),
|
||||||
|
positive: intl.formatMessage(commonMessages.yes)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
active: opts.visibleInStorefront.active
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
export { default } from "./AttributeListPage";
|
export { default } from "./AttributeListPage";
|
||||||
export * from "./AttributeListPage";
|
export * from "./AttributeListPage";
|
||||||
|
export * from "./filters";
|
||||||
|
|
|
@ -15,6 +15,12 @@ import {
|
||||||
export const attributeSection = "/attributes/";
|
export const attributeSection = "/attributes/";
|
||||||
|
|
||||||
export enum AttributeListUrlFiltersEnum {
|
export enum AttributeListUrlFiltersEnum {
|
||||||
|
availableInGrid = "availableInGrid",
|
||||||
|
filterableInDashboard = "filterableInDashboard",
|
||||||
|
filterableInStorefront = "filterableInStorefront",
|
||||||
|
isVariantOnly = "isVariantOnly",
|
||||||
|
valueRequired = "valueRequired",
|
||||||
|
visibleInStorefront = "visibleInStorefront",
|
||||||
query = "query"
|
query = "query"
|
||||||
}
|
}
|
||||||
export type AttributeListUrlFilters = Filters<AttributeListUrlFiltersEnum>;
|
export type AttributeListUrlFilters = Filters<AttributeListUrlFiltersEnum>;
|
||||||
|
|
|
@ -9,7 +9,8 @@ import {
|
||||||
getActiveFilters,
|
getActiveFilters,
|
||||||
getFilterTabs,
|
getFilterTabs,
|
||||||
getFilterVariables,
|
getFilterVariables,
|
||||||
saveFilterTab
|
saveFilterTab,
|
||||||
|
getFilterOpts
|
||||||
} from "@saleor/attributes/views/AttributeList/filters";
|
} from "@saleor/attributes/views/AttributeList/filters";
|
||||||
import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
|
import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
|
||||||
import SaveFilterTabDialog, {
|
import SaveFilterTabDialog, {
|
||||||
|
@ -24,6 +25,8 @@ import usePaginator, {
|
||||||
import { getSortParams } from "@saleor/utils/sort";
|
import { getSortParams } from "@saleor/utils/sort";
|
||||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||||
|
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||||
|
import useShop from "@saleor/hooks/useShop";
|
||||||
import { PAGINATE_BY } from "../../../config";
|
import { PAGINATE_BY } from "../../../config";
|
||||||
import useBulkActions from "../../../hooks/useBulkActions";
|
import useBulkActions from "../../../hooks/useBulkActions";
|
||||||
import { maybe } from "../../../misc";
|
import { maybe } from "../../../misc";
|
||||||
|
@ -35,12 +38,12 @@ import { AttributeBulkDelete } from "../../types/AttributeBulkDelete";
|
||||||
import {
|
import {
|
||||||
attributeAddUrl,
|
attributeAddUrl,
|
||||||
attributeListUrl,
|
attributeListUrl,
|
||||||
AttributeListUrlFilters,
|
|
||||||
AttributeListUrlQueryParams,
|
AttributeListUrlQueryParams,
|
||||||
attributeUrl,
|
attributeUrl,
|
||||||
AttributeListUrlDialog
|
AttributeListUrlDialog
|
||||||
} from "../../urls";
|
} from "../../urls";
|
||||||
import { getSortQueryVariables } from "./sort";
|
import { getSortQueryVariables } from "./sort";
|
||||||
|
import { getFilterQueryParam } from "./filters";
|
||||||
|
|
||||||
interface AttributeListProps {
|
interface AttributeListProps {
|
||||||
params: AttributeListUrlQueryParams;
|
params: AttributeListUrlQueryParams;
|
||||||
|
@ -50,6 +53,7 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
|
||||||
const navigate = useNavigator();
|
const navigate = useNavigator();
|
||||||
const paginate = usePaginator();
|
const paginate = usePaginator();
|
||||||
const notify = useNotifier();
|
const notify = useNotifier();
|
||||||
|
const shop = useShop();
|
||||||
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
||||||
params.ids
|
params.ids
|
||||||
);
|
);
|
||||||
|
@ -82,16 +86,17 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
|
||||||
AttributeListUrlQueryParams
|
AttributeListUrlQueryParams
|
||||||
>(navigate, attributeListUrl, params);
|
>(navigate, attributeListUrl, params);
|
||||||
|
|
||||||
const changeFilterField = (filter: AttributeListUrlFilters) => {
|
const [
|
||||||
reset();
|
changeFilters,
|
||||||
navigate(
|
resetFilters,
|
||||||
attributeListUrl({
|
handleSearchChange
|
||||||
...getActiveFilters(params),
|
] = createFilterHandlers({
|
||||||
...filter,
|
cleanupFn: reset,
|
||||||
activeTab: undefined
|
createUrl: attributeListUrl,
|
||||||
})
|
getFilterQueryParam,
|
||||||
);
|
navigate,
|
||||||
};
|
params
|
||||||
|
});
|
||||||
|
|
||||||
const handleTabChange = (tab: number) => {
|
const handleTabChange = (tab: number) => {
|
||||||
reset();
|
reset();
|
||||||
|
@ -135,6 +140,7 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSort = createSortHandler(navigate, attributeListUrl, params);
|
const handleSort = createSortHandler(navigate, attributeListUrl, params);
|
||||||
|
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AttributeBulkDeleteMutation onCompleted={handleBulkDelete}>
|
<AttributeBulkDeleteMutation onCompleted={handleBulkDelete}>
|
||||||
|
@ -144,17 +150,20 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
|
||||||
attributes={maybe(() =>
|
attributes={maybe(() =>
|
||||||
data.attributes.edges.map(edge => edge.node)
|
data.attributes.edges.map(edge => edge.node)
|
||||||
)}
|
)}
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
disabled={loading || attributeBulkDeleteOpts.loading}
|
disabled={loading || attributeBulkDeleteOpts.loading}
|
||||||
|
filterOpts={getFilterOpts(params)}
|
||||||
initialSearch={params.query || ""}
|
initialSearch={params.query || ""}
|
||||||
isChecked={isSelected}
|
isChecked={isSelected}
|
||||||
onAdd={() => navigate(attributeAddUrl())}
|
onAdd={() => navigate(attributeAddUrl())}
|
||||||
onAll={() => navigate(attributeListUrl())}
|
onAll={resetFilters}
|
||||||
onBack={() => navigate(configurationMenuUrl)}
|
onBack={() => navigate(configurationMenuUrl)}
|
||||||
|
onFilterChange={changeFilters}
|
||||||
onNextPage={loadNextPage}
|
onNextPage={loadNextPage}
|
||||||
onPreviousPage={loadPreviousPage}
|
onPreviousPage={loadPreviousPage}
|
||||||
onRowClick={id => () => navigate(attributeUrl(id))}
|
onRowClick={id => () => navigate(attributeUrl(id))}
|
||||||
onSearchChange={query => changeFilterField({ query })}
|
onSearchChange={handleSearchChange}
|
||||||
onSort={handleSort}
|
onSort={handleSort}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
onTabDelete={() => openModal("delete-search")}
|
onTabDelete={() => openModal("delete-search")}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
|
||||||
|
Object {
|
||||||
|
"availableInGrid": "true",
|
||||||
|
"filterableInDashboard": "true",
|
||||||
|
"filterableInStorefront": "true",
|
||||||
|
"isVariantOnly": "true",
|
||||||
|
"valueRequired": "true",
|
||||||
|
"visibleInStorefront": "true",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"availableInGrid=true&filterableInDashboard=true&filterableInStorefront=true&isVariantOnly=true&valueRequired=true&visibleInStorefront=true"`;
|
77
src/attributes/views/AttributeList/filters.test.ts
Normal file
77
src/attributes/views/AttributeList/filters.test.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { createIntl } from "react-intl";
|
||||||
|
import { stringify as stringifyQs } from "qs";
|
||||||
|
|
||||||
|
import { AttributeListUrlFilters } from "@saleor/attributes/urls";
|
||||||
|
import { createFilterStructure } from "@saleor/attributes/components/AttributeListPage";
|
||||||
|
import { getFilterQueryParams } from "@saleor/utils/filters";
|
||||||
|
import { config } from "@test/intl";
|
||||||
|
import { getExistingKeys, setFilterOptsStatus } from "@test/filters";
|
||||||
|
import { getFilterVariables, getFilterQueryParam } from "./filters";
|
||||||
|
|
||||||
|
describe("Filtering query params", () => {
|
||||||
|
it("should be empty object if no params given", () => {
|
||||||
|
const params: AttributeListUrlFilters = {};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterVariables)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be empty object if params given", () => {
|
||||||
|
const params: AttributeListUrlFilters = {
|
||||||
|
availableInGrid: true.toString()
|
||||||
|
};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterVariables)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Filtering URL params", () => {
|
||||||
|
const intl = createIntl(config);
|
||||||
|
|
||||||
|
const filters = createFilterStructure(intl, {
|
||||||
|
availableInGrid: {
|
||||||
|
active: false,
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
filterableInDashboard: {
|
||||||
|
active: false,
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
filterableInStorefront: {
|
||||||
|
active: false,
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
isVariantOnly: {
|
||||||
|
active: false,
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
valueRequired: {
|
||||||
|
active: false,
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
visibleInStorefront: {
|
||||||
|
active: false,
|
||||||
|
value: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be empty if no active filters", () => {
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
filters,
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterQueryParams)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be empty if active filters are present", () => {
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
setFilterOptsStatus(filters, true),
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(filterQueryParams).toMatchSnapshot();
|
||||||
|
expect(stringifyQs(filterQueryParams)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,7 +1,14 @@
|
||||||
import { AttributeFilterInput } from "@saleor/types/globalTypes";
|
import { AttributeFilterInput } from "@saleor/types/globalTypes";
|
||||||
|
import { maybe, parseBoolean } from "@saleor/misc";
|
||||||
|
import { IFilterElement } from "@saleor/components/Filter";
|
||||||
|
import {
|
||||||
|
AttributeListFilterOpts,
|
||||||
|
AttributeFilterKeys
|
||||||
|
} from "@saleor/attributes/components/AttributeListPage";
|
||||||
import {
|
import {
|
||||||
createFilterTabUtils,
|
createFilterTabUtils,
|
||||||
createFilterUtils
|
createFilterUtils,
|
||||||
|
getSingleValueQueryParam
|
||||||
} from "../../../utils/filters";
|
} from "../../../utils/filters";
|
||||||
import {
|
import {
|
||||||
AttributeListUrlFilters,
|
AttributeListUrlFilters,
|
||||||
|
@ -11,14 +18,113 @@ import {
|
||||||
|
|
||||||
export const PRODUCT_FILTERS_KEY = "productFilters";
|
export const PRODUCT_FILTERS_KEY = "productFilters";
|
||||||
|
|
||||||
|
export function getFilterOpts(
|
||||||
|
params: AttributeListUrlFilters
|
||||||
|
): AttributeListFilterOpts {
|
||||||
|
return {
|
||||||
|
availableInGrid: {
|
||||||
|
active: params.availableInGrid !== undefined,
|
||||||
|
value: maybe(() => parseBoolean(params.availableInGrid, true))
|
||||||
|
},
|
||||||
|
filterableInDashboard: {
|
||||||
|
active: params.filterableInDashboard !== undefined,
|
||||||
|
value: maybe(() => parseBoolean(params.filterableInDashboard, true))
|
||||||
|
},
|
||||||
|
filterableInStorefront: {
|
||||||
|
active: params.filterableInStorefront !== undefined,
|
||||||
|
value: maybe(() => parseBoolean(params.filterableInStorefront, true))
|
||||||
|
},
|
||||||
|
isVariantOnly: {
|
||||||
|
active: params.isVariantOnly !== undefined,
|
||||||
|
value: maybe(() => parseBoolean(params.isVariantOnly, true))
|
||||||
|
},
|
||||||
|
valueRequired: {
|
||||||
|
active: params.valueRequired !== undefined,
|
||||||
|
value: maybe(() => parseBoolean(params.valueRequired, true))
|
||||||
|
},
|
||||||
|
visibleInStorefront: {
|
||||||
|
active: params.visibleInStorefront !== undefined,
|
||||||
|
value: maybe(() => parseBoolean(params.visibleInStorefront, true))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getFilterVariables(
|
export function getFilterVariables(
|
||||||
params: AttributeListUrlFilters
|
params: AttributeListUrlFilters
|
||||||
): AttributeFilterInput {
|
): AttributeFilterInput {
|
||||||
return {
|
return {
|
||||||
search: params.query
|
availableInGrid:
|
||||||
|
params.availableInGrid !== undefined
|
||||||
|
? parseBoolean(params.availableInGrid, false)
|
||||||
|
: undefined,
|
||||||
|
filterableInDashboard:
|
||||||
|
params.filterableInDashboard !== undefined
|
||||||
|
? parseBoolean(params.filterableInDashboard, false)
|
||||||
|
: undefined,
|
||||||
|
filterableInStorefront:
|
||||||
|
params.filterableInStorefront !== undefined
|
||||||
|
? parseBoolean(params.filterableInStorefront, false)
|
||||||
|
: undefined,
|
||||||
|
isVariantOnly:
|
||||||
|
params.isVariantOnly !== undefined
|
||||||
|
? parseBoolean(params.isVariantOnly, false)
|
||||||
|
: undefined,
|
||||||
|
search: params.query,
|
||||||
|
valueRequired:
|
||||||
|
params.valueRequired !== undefined
|
||||||
|
? parseBoolean(params.valueRequired, false)
|
||||||
|
: undefined,
|
||||||
|
visibleInStorefront:
|
||||||
|
params.visibleInStorefront !== undefined
|
||||||
|
? parseBoolean(params.visibleInStorefront, false)
|
||||||
|
: undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFilterQueryParam(
|
||||||
|
filter: IFilterElement<AttributeFilterKeys>
|
||||||
|
): AttributeListUrlFilters {
|
||||||
|
const { name } = filter;
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case AttributeFilterKeys.availableInGrid:
|
||||||
|
return getSingleValueQueryParam(
|
||||||
|
filter,
|
||||||
|
AttributeListUrlFiltersEnum.availableInGrid
|
||||||
|
);
|
||||||
|
|
||||||
|
case AttributeFilterKeys.filterableInDashboard:
|
||||||
|
return getSingleValueQueryParam(
|
||||||
|
filter,
|
||||||
|
AttributeListUrlFiltersEnum.filterableInDashboard
|
||||||
|
);
|
||||||
|
|
||||||
|
case AttributeFilterKeys.filterableInStorefront:
|
||||||
|
return getSingleValueQueryParam(
|
||||||
|
filter,
|
||||||
|
AttributeListUrlFiltersEnum.filterableInStorefront
|
||||||
|
);
|
||||||
|
|
||||||
|
case AttributeFilterKeys.isVariantOnly:
|
||||||
|
return getSingleValueQueryParam(
|
||||||
|
filter,
|
||||||
|
AttributeListUrlFiltersEnum.isVariantOnly
|
||||||
|
);
|
||||||
|
|
||||||
|
case AttributeFilterKeys.valueRequired:
|
||||||
|
return getSingleValueQueryParam(
|
||||||
|
filter,
|
||||||
|
AttributeListUrlFiltersEnum.valueRequired
|
||||||
|
);
|
||||||
|
|
||||||
|
case AttributeFilterKeys.visibleInStorefront:
|
||||||
|
return getSingleValueQueryParam(
|
||||||
|
filter,
|
||||||
|
AttributeListUrlFiltersEnum.visibleInStorefront
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
deleteFilterTab,
|
deleteFilterTab,
|
||||||
getFilterTabs,
|
getFilterTabs,
|
||||||
|
|
|
@ -5,34 +5,42 @@ import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import { Container } from "@saleor/components/Container";
|
import { Container } from "@saleor/components/Container";
|
||||||
import PageHeader from "@saleor/components/PageHeader";
|
import PageHeader from "@saleor/components/PageHeader";
|
||||||
import SearchBar from "@saleor/components/SearchBar";
|
import FilterBar from "@saleor/components/FilterBar";
|
||||||
import { sectionNames } from "@saleor/intl";
|
import { sectionNames } from "@saleor/intl";
|
||||||
import {
|
import {
|
||||||
ListActions,
|
ListActions,
|
||||||
PageListProps,
|
PageListProps,
|
||||||
SearchPageProps,
|
FilterPageProps,
|
||||||
TabPageProps,
|
TabPageProps,
|
||||||
SortPage
|
SortPage
|
||||||
} from "@saleor/types";
|
} from "@saleor/types";
|
||||||
import { CollectionListUrlSortField } from "@saleor/collections/urls";
|
import { CollectionListUrlSortField } from "@saleor/collections/urls";
|
||||||
import { CollectionList_collections_edges_node } from "../../types/CollectionList";
|
import { CollectionList_collections_edges_node } from "../../types/CollectionList";
|
||||||
import CollectionList from "../CollectionList/CollectionList";
|
import CollectionList from "../CollectionList/CollectionList";
|
||||||
|
import {
|
||||||
|
CollectionFilterKeys,
|
||||||
|
CollectionListFilterOpts,
|
||||||
|
createFilterStructure
|
||||||
|
} from "./filters";
|
||||||
|
|
||||||
export interface CollectionListPageProps
|
export interface CollectionListPageProps
|
||||||
extends PageListProps,
|
extends PageListProps,
|
||||||
ListActions,
|
ListActions,
|
||||||
SearchPageProps,
|
FilterPageProps<CollectionFilterKeys, CollectionListFilterOpts>,
|
||||||
SortPage<CollectionListUrlSortField>,
|
SortPage<CollectionListUrlSortField>,
|
||||||
TabPageProps {
|
TabPageProps {
|
||||||
collections: CollectionList_collections_edges_node[];
|
collections: CollectionList_collections_edges_node[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const CollectionListPage: React.FC<CollectionListPageProps> = ({
|
const CollectionListPage: React.FC<CollectionListPageProps> = ({
|
||||||
|
currencySymbol,
|
||||||
currentTab,
|
currentTab,
|
||||||
disabled,
|
disabled,
|
||||||
|
filterOpts,
|
||||||
initialSearch,
|
initialSearch,
|
||||||
onAdd,
|
onAdd,
|
||||||
onAll,
|
onAll,
|
||||||
|
onFilterChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
onTabDelete,
|
onTabDelete,
|
||||||
|
@ -42,6 +50,8 @@ const CollectionListPage: React.FC<CollectionListPageProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const structure = createFilterStructure(intl, filterOpts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<PageHeader title={intl.formatMessage(sectionNames.collections)}>
|
<PageHeader title={intl.formatMessage(sectionNames.collections)}>
|
||||||
|
@ -58,18 +68,21 @@ const CollectionListPage: React.FC<CollectionListPageProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Card>
|
<Card>
|
||||||
<SearchBar
|
<FilterBar
|
||||||
allTabLabel={intl.formatMessage({
|
allTabLabel={intl.formatMessage({
|
||||||
defaultMessage: "All Collections",
|
defaultMessage: "All Collections",
|
||||||
description: "tab name"
|
description: "tab name"
|
||||||
})}
|
})}
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
filterStructure={structure}
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
searchPlaceholder={intl.formatMessage({
|
searchPlaceholder={intl.formatMessage({
|
||||||
defaultMessage: "Search Collection"
|
defaultMessage: "Search Collection"
|
||||||
})}
|
})}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
onAll={onAll}
|
onAll={onAll}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
onTabChange={onTabChange}
|
onTabChange={onTabChange}
|
||||||
onTabDelete={onTabDelete}
|
onTabDelete={onTabDelete}
|
||||||
|
|
53
src/collections/components/CollectionListPage/filters.ts
Normal file
53
src/collections/components/CollectionListPage/filters.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { defineMessages, IntlShape } from "react-intl";
|
||||||
|
|
||||||
|
import { commonMessages } from "@saleor/intl";
|
||||||
|
import { FilterOpts } from "@saleor/types";
|
||||||
|
import { CollectionPublished } from "@saleor/types/globalTypes";
|
||||||
|
import { IFilter } from "@saleor/components/Filter";
|
||||||
|
import { createOptionsField } from "@saleor/utils/filters/fields";
|
||||||
|
|
||||||
|
export interface CollectionListFilterOpts {
|
||||||
|
status: FilterOpts<CollectionPublished>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CollectionFilterKeys {
|
||||||
|
status = "status"
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
hidden: {
|
||||||
|
defaultMessage: "Hidden",
|
||||||
|
description: "collection"
|
||||||
|
},
|
||||||
|
published: {
|
||||||
|
defaultMessage: "Published",
|
||||||
|
description: "collection"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createFilterStructure(
|
||||||
|
intl: IntlShape,
|
||||||
|
opts: CollectionListFilterOpts
|
||||||
|
): IFilter<CollectionFilterKeys> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...createOptionsField(
|
||||||
|
CollectionFilterKeys.status,
|
||||||
|
intl.formatMessage(commonMessages.status),
|
||||||
|
[opts.status.value],
|
||||||
|
false,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.published),
|
||||||
|
value: CollectionPublished.PUBLISHED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.hidden),
|
||||||
|
value: CollectionPublished.HIDDEN
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
active: opts.status.active
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
export { default } from "./CollectionListPage";
|
export { default } from "./CollectionListPage";
|
||||||
export * from "./CollectionListPage";
|
export * from "./CollectionListPage";
|
||||||
|
export * from "./filters";
|
||||||
|
|
|
@ -15,6 +15,7 @@ const collectionSectionUrl = "/collections/";
|
||||||
|
|
||||||
export const collectionListPath = collectionSectionUrl;
|
export const collectionListPath = collectionSectionUrl;
|
||||||
export enum CollectionListUrlFiltersEnum {
|
export enum CollectionListUrlFiltersEnum {
|
||||||
|
status = "status",
|
||||||
query = "query"
|
query = "query"
|
||||||
}
|
}
|
||||||
export type CollectionListUrlFilters = Filters<CollectionListUrlFiltersEnum>;
|
export type CollectionListUrlFilters = Filters<CollectionListUrlFiltersEnum>;
|
||||||
|
|
|
@ -23,6 +23,8 @@ import { ListViews } from "@saleor/types";
|
||||||
import { getSortParams } from "@saleor/utils/sort";
|
import { getSortParams } from "@saleor/utils/sort";
|
||||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||||
|
import useShop from "@saleor/hooks/useShop";
|
||||||
|
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||||
import CollectionListPage from "../../components/CollectionListPage/CollectionListPage";
|
import CollectionListPage from "../../components/CollectionListPage/CollectionListPage";
|
||||||
import {
|
import {
|
||||||
TypedCollectionBulkDelete,
|
TypedCollectionBulkDelete,
|
||||||
|
@ -34,7 +36,6 @@ import { CollectionBulkPublish } from "../../types/CollectionBulkPublish";
|
||||||
import {
|
import {
|
||||||
collectionAddUrl,
|
collectionAddUrl,
|
||||||
collectionListUrl,
|
collectionListUrl,
|
||||||
CollectionListUrlFilters,
|
|
||||||
CollectionListUrlQueryParams,
|
CollectionListUrlQueryParams,
|
||||||
collectionUrl,
|
collectionUrl,
|
||||||
CollectionListUrlDialog
|
CollectionListUrlDialog
|
||||||
|
@ -45,8 +46,10 @@ import {
|
||||||
getActiveFilters,
|
getActiveFilters,
|
||||||
getFilterTabs,
|
getFilterTabs,
|
||||||
getFilterVariables,
|
getFilterVariables,
|
||||||
saveFilterTab
|
saveFilterTab,
|
||||||
} from "./filter";
|
getFilterQueryParam,
|
||||||
|
getFilterOpts
|
||||||
|
} from "./filters";
|
||||||
import { getSortQueryVariables } from "./sort";
|
import { getSortQueryVariables } from "./sort";
|
||||||
|
|
||||||
interface CollectionListProps {
|
interface CollectionListProps {
|
||||||
|
@ -57,6 +60,7 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
|
||||||
const navigate = useNavigator();
|
const navigate = useNavigator();
|
||||||
const notify = useNotifier();
|
const notify = useNotifier();
|
||||||
const paginate = usePaginator();
|
const paginate = usePaginator();
|
||||||
|
const shop = useShop();
|
||||||
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
||||||
params.ids
|
params.ids
|
||||||
);
|
);
|
||||||
|
@ -88,16 +92,17 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
|
||||||
: 0
|
: 0
|
||||||
: parseInt(params.activeTab, 0);
|
: parseInt(params.activeTab, 0);
|
||||||
|
|
||||||
const changeFilterField = (filter: CollectionListUrlFilters) => {
|
const [
|
||||||
reset();
|
changeFilters,
|
||||||
navigate(
|
resetFilters,
|
||||||
collectionListUrl({
|
handleSearchChange
|
||||||
...getActiveFilters(params),
|
] = createFilterHandlers({
|
||||||
...filter,
|
cleanupFn: reset,
|
||||||
activeTab: undefined
|
createUrl: collectionListUrl,
|
||||||
})
|
getFilterQueryParam,
|
||||||
);
|
navigate,
|
||||||
};
|
params
|
||||||
|
});
|
||||||
|
|
||||||
const [openModal, closeModal] = createDialogActionHandlers<
|
const [openModal, closeModal] = createDialogActionHandlers<
|
||||||
CollectionListUrlDialog,
|
CollectionListUrlDialog,
|
||||||
|
@ -154,6 +159,7 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSort = createSortHandler(navigate, collectionListUrl, params);
|
const handleSort = createSortHandler(navigate, collectionListUrl, params);
|
||||||
|
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TypedCollectionBulkDelete onCompleted={handleCollectionBulkDelete}>
|
<TypedCollectionBulkDelete onCompleted={handleCollectionBulkDelete}>
|
||||||
|
@ -162,11 +168,14 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
|
||||||
{(collectionBulkPublish, collectionBulkPublishOpts) => (
|
{(collectionBulkPublish, collectionBulkPublishOpts) => (
|
||||||
<>
|
<>
|
||||||
<CollectionListPage
|
<CollectionListPage
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
filterOpts={getFilterOpts(params)}
|
||||||
initialSearch={params.query || ""}
|
initialSearch={params.query || ""}
|
||||||
onSearchChange={query => changeFilterField({ query })}
|
onSearchChange={handleSearchChange}
|
||||||
|
onFilterChange={changeFilters}
|
||||||
onAdd={() => navigate(collectionAddUrl)}
|
onAdd={() => navigate(collectionAddUrl)}
|
||||||
onAll={() => navigate(collectionListUrl())}
|
onAll={resetFilters}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
onTabDelete={() => openModal("delete-search")}
|
onTabDelete={() => openModal("delete-search")}
|
||||||
onTabSave={() => openModal("save-search")}
|
onTabSave={() => openModal("save-search")}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
|
||||||
|
Object {
|
||||||
|
"status": "PUBLISHED",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"status=PUBLISHED"`;
|
|
@ -1,31 +0,0 @@
|
||||||
import { CollectionFilterInput } from "@saleor/types/globalTypes";
|
|
||||||
import {
|
|
||||||
createFilterTabUtils,
|
|
||||||
createFilterUtils
|
|
||||||
} from "../../../utils/filters";
|
|
||||||
import {
|
|
||||||
CollectionListUrlFilters,
|
|
||||||
CollectionListUrlFiltersEnum,
|
|
||||||
CollectionListUrlQueryParams
|
|
||||||
} from "../../urls";
|
|
||||||
|
|
||||||
export const COLLECTION_FILTERS_KEY = "collectionFilters";
|
|
||||||
|
|
||||||
export function getFilterVariables(
|
|
||||||
params: CollectionListUrlFilters
|
|
||||||
): CollectionFilterInput {
|
|
||||||
return {
|
|
||||||
search: params.query
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const {
|
|
||||||
deleteFilterTab,
|
|
||||||
getFilterTabs,
|
|
||||||
saveFilterTab
|
|
||||||
} = createFilterTabUtils<CollectionListUrlFilters>(COLLECTION_FILTERS_KEY);
|
|
||||||
|
|
||||||
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
|
|
||||||
CollectionListUrlQueryParams,
|
|
||||||
CollectionListUrlFilters
|
|
||||||
>(CollectionListUrlFiltersEnum);
|
|
65
src/collections/views/CollectionList/filters.test.ts
Normal file
65
src/collections/views/CollectionList/filters.test.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { createIntl } from "react-intl";
|
||||||
|
import { stringify as stringifyQs } from "qs";
|
||||||
|
|
||||||
|
import { CollectionListUrlFilters } from "@saleor/collections/urls";
|
||||||
|
import { createFilterStructure } from "@saleor/collections/components/CollectionListPage";
|
||||||
|
import { getFilterQueryParams } from "@saleor/utils/filters";
|
||||||
|
import { CollectionPublished } from "@saleor/types/globalTypes";
|
||||||
|
import { config } from "@test/intl";
|
||||||
|
import { getExistingKeys, setFilterOptsStatus } from "@test/filters";
|
||||||
|
import { getFilterVariables, getFilterQueryParam } from "./filters";
|
||||||
|
|
||||||
|
describe("Filtering query params", () => {
|
||||||
|
it("should be empty object if no params given", () => {
|
||||||
|
const params: CollectionListUrlFilters = {};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterVariables)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be empty object if params given", () => {
|
||||||
|
const params: CollectionListUrlFilters = {
|
||||||
|
status: CollectionPublished.PUBLISHED
|
||||||
|
};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterVariables)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Filtering URL params", () => {
|
||||||
|
const intl = createIntl(config);
|
||||||
|
|
||||||
|
const filters = createFilterStructure(intl, {
|
||||||
|
status: {
|
||||||
|
active: false,
|
||||||
|
value: CollectionPublished.PUBLISHED
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be empty if no active filters", () => {
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
filters,
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterQueryParams)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be empty if active filters are present", () => {
|
||||||
|
const filters = createFilterStructure(intl, {
|
||||||
|
status: {
|
||||||
|
active: true,
|
||||||
|
value: CollectionPublished.PUBLISHED
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
setFilterOptsStatus(filters, true),
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(filterQueryParams).toMatchSnapshot();
|
||||||
|
expect(stringifyQs(filterQueryParams)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
70
src/collections/views/CollectionList/filters.ts
Normal file
70
src/collections/views/CollectionList/filters.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import {
|
||||||
|
CollectionFilterInput,
|
||||||
|
CollectionPublished
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
|
import { IFilterElement } from "@saleor/components/Filter";
|
||||||
|
import { maybe, findValueInEnum } from "@saleor/misc";
|
||||||
|
import {
|
||||||
|
CollectionListFilterOpts,
|
||||||
|
CollectionFilterKeys
|
||||||
|
} from "@saleor/collections/components/CollectionListPage";
|
||||||
|
import {
|
||||||
|
CollectionListUrlFilters,
|
||||||
|
CollectionListUrlFiltersEnum,
|
||||||
|
CollectionListUrlQueryParams
|
||||||
|
} from "../../urls";
|
||||||
|
import {
|
||||||
|
createFilterTabUtils,
|
||||||
|
createFilterUtils,
|
||||||
|
getSingleEnumValueQueryParam
|
||||||
|
} from "../../../utils/filters";
|
||||||
|
|
||||||
|
export const COLLECTION_FILTERS_KEY = "collectionFilters";
|
||||||
|
|
||||||
|
export function getFilterOpts(
|
||||||
|
params: CollectionListUrlFilters
|
||||||
|
): CollectionListFilterOpts {
|
||||||
|
return {
|
||||||
|
status: {
|
||||||
|
active: maybe(() => params.status !== undefined, false),
|
||||||
|
value: maybe(() => findValueInEnum(status, CollectionPublished))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilterVariables(
|
||||||
|
params: CollectionListUrlFilters
|
||||||
|
): CollectionFilterInput {
|
||||||
|
return {
|
||||||
|
published: params.status
|
||||||
|
? findValueInEnum(params.status, CollectionPublished)
|
||||||
|
: undefined,
|
||||||
|
search: params.query
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilterQueryParam(
|
||||||
|
filter: IFilterElement<CollectionFilterKeys>
|
||||||
|
): CollectionListUrlFilters {
|
||||||
|
const { name } = filter;
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case CollectionFilterKeys.status:
|
||||||
|
return getSingleEnumValueQueryParam(
|
||||||
|
filter,
|
||||||
|
CollectionListUrlFiltersEnum.status,
|
||||||
|
CollectionPublished
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const {
|
||||||
|
deleteFilterTab,
|
||||||
|
getFilterTabs,
|
||||||
|
saveFilterTab
|
||||||
|
} = createFilterTabUtils<CollectionListUrlFilters>(COLLECTION_FILTERS_KEY);
|
||||||
|
|
||||||
|
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
|
||||||
|
CollectionListUrlQueryParams,
|
||||||
|
CollectionListUrlFilters
|
||||||
|
>(CollectionListUrlFiltersEnum);
|
|
@ -47,7 +47,6 @@ const useStyles = makeStyles(
|
||||||
},
|
},
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
gridColumn: 2,
|
gridColumn: 2,
|
||||||
height: 70,
|
|
||||||
position: "sticky",
|
position: "sticky",
|
||||||
zIndex: 10
|
zIndex: 10
|
||||||
},
|
},
|
||||||
|
|
22
src/components/Filter/Arrow.tsx
Normal file
22
src/components/Filter/Arrow.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const Arrow: React.FC<React.SVGProps<SVGSVGElement>> = props => (
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="21"
|
||||||
|
viewBox="0 0 18 21"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M13.5858 17.1357L-1.37065e-07 17.1357L-1.37065e-07 15L-1.37064e-07 0L2 -8.74228e-08L2 15.1357L13.5858 15.1357L11.8643 13.4142L13.2785 12L17.4142 16.1357L13.2785 20.2714L11.8643 18.8571L13.5858 17.1357Z"
|
||||||
|
fill="#3D3D3D"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
Arrow.displayName = "Arrow";
|
||||||
|
export default Arrow;
|
|
@ -1,24 +1,22 @@
|
||||||
import ButtonBase from "@material-ui/core/ButtonBase";
|
import ButtonBase from "@material-ui/core/ButtonBase";
|
||||||
|
import ClickAwayListener from "@material-ui/core/ClickAwayListener";
|
||||||
import Grow from "@material-ui/core/Grow";
|
import Grow from "@material-ui/core/Grow";
|
||||||
import Paper from "@material-ui/core/Paper";
|
|
||||||
import Popper from "@material-ui/core/Popper";
|
import Popper from "@material-ui/core/Popper";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
import { fade } from "@material-ui/core/styles/colorManipulator";
|
||||||
import Typography from "@material-ui/core/Typography";
|
import Typography from "@material-ui/core/Typography";
|
||||||
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { FilterContentSubmitData } from "./FilterContent";
|
import { IFilter, IFilterElement } from "./types";
|
||||||
import { IFilter } from "./types";
|
import useFilter from "./useFilter";
|
||||||
import { FilterContent } from ".";
|
import { FilterContent } from ".";
|
||||||
|
|
||||||
export interface FilterProps<TFilterKeys = string> {
|
export interface FilterProps<TFilterKeys extends string = string> {
|
||||||
currencySymbol: string;
|
currencySymbol: string;
|
||||||
menu: IFilter<TFilterKeys>;
|
menu: IFilter<TFilterKeys>;
|
||||||
filterLabel: string;
|
onFilterAdd: (filter: Array<IFilterElement<string>>) => void;
|
||||||
onFilterAdd: (filter: FilterContentSubmitData) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
|
@ -49,7 +47,6 @@ const useStyles = makeStyles(
|
||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: 600 as 600,
|
fontWeight: 600 as 600,
|
||||||
marginRight: 4,
|
|
||||||
textTransform: "uppercase"
|
textTransform: "uppercase"
|
||||||
},
|
},
|
||||||
filterButton: {
|
filterButton: {
|
||||||
|
@ -73,70 +70,106 @@ const useStyles = makeStyles(
|
||||||
width: 240
|
width: 240
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
zIndex: 1
|
width: 376,
|
||||||
|
zIndex: 3
|
||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
transform: "rotate(180deg)"
|
transform: "rotate(180deg)"
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
display: "inline-block",
|
||||||
|
height: 28,
|
||||||
|
margin: theme.spacing(0, 1.5, 0, 1),
|
||||||
|
width: 1
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{ name: "Filter" }
|
{ name: "Filter" }
|
||||||
);
|
);
|
||||||
const Filter: React.FC<FilterProps> = props => {
|
const Filter: React.FC<FilterProps> = props => {
|
||||||
const { currencySymbol, filterLabel, menu, onFilterAdd } = props;
|
const { currencySymbol, menu, onFilterAdd } = props;
|
||||||
const classes = useStyles(props);
|
const classes = useStyles(props);
|
||||||
|
|
||||||
const anchor = React.useRef<HTMLDivElement>();
|
const anchor = React.useRef<HTMLDivElement>();
|
||||||
const [isFilterMenuOpened, setFilterMenuOpened] = React.useState(false);
|
const [isFilterMenuOpened, setFilterMenuOpened] = React.useState(false);
|
||||||
|
const [data, dispatch, reset] = useFilter(menu);
|
||||||
|
|
||||||
|
const isFilterActive = menu.some(filterElement => filterElement.active);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={anchor}>
|
<ClickAwayListener
|
||||||
<ButtonBase
|
onClickAway={event => {
|
||||||
className={classNames(classes.filterButton, classes.addFilterButton, {
|
if ((event.target as HTMLElement).getAttribute("role") !== "option") {
|
||||||
[classes.addFilterButtonActive]: isFilterMenuOpened
|
setFilterMenuOpened(false);
|
||||||
})}
|
}
|
||||||
onClick={() => setFilterMenuOpened(!isFilterMenuOpened)}
|
}}
|
||||||
>
|
>
|
||||||
<Typography className={classes.addFilterText}>
|
<div ref={anchor}>
|
||||||
<FormattedMessage defaultMessage="Add Filter" description="button" />
|
<ButtonBase
|
||||||
</Typography>
|
className={classNames(classes.filterButton, classes.addFilterButton, {
|
||||||
<ArrowDropDownIcon
|
[classes.addFilterButtonActive]:
|
||||||
color="primary"
|
isFilterMenuOpened || isFilterActive
|
||||||
className={classNames(classes.addFilterIcon, {
|
|
||||||
[classes.rotate]: isFilterMenuOpened
|
|
||||||
})}
|
})}
|
||||||
/>
|
onClick={() => setFilterMenuOpened(!isFilterMenuOpened)}
|
||||||
</ButtonBase>
|
>
|
||||||
<Popper
|
<Typography className={classes.addFilterText}>
|
||||||
className={classes.popover}
|
<FormattedMessage defaultMessage="Filters" description="button" />
|
||||||
open={isFilterMenuOpened}
|
</Typography>
|
||||||
anchorEl={anchor.current}
|
{isFilterActive && (
|
||||||
transition
|
<>
|
||||||
disablePortal
|
<span className={classes.separator} />
|
||||||
placement="bottom-start"
|
<Typography className={classes.addFilterText}>
|
||||||
>
|
{menu.reduce(
|
||||||
{({ TransitionProps, placement }) => (
|
(acc, filterElement) => acc + (filterElement.active ? 1 : 0),
|
||||||
<Grow
|
0
|
||||||
{...TransitionProps}
|
)}
|
||||||
style={{
|
</Typography>
|
||||||
transformOrigin:
|
</>
|
||||||
placement === "bottom" ? "right top" : "right bottom"
|
)}
|
||||||
}}
|
</ButtonBase>
|
||||||
>
|
<Popper
|
||||||
<Paper className={classes.paper}>
|
className={classes.popover}
|
||||||
<Typography>{filterLabel}</Typography>
|
open={isFilterMenuOpened}
|
||||||
|
anchorEl={anchor.current}
|
||||||
|
transition
|
||||||
|
disablePortal
|
||||||
|
placement="bottom-start"
|
||||||
|
modifiers={{
|
||||||
|
flip: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
hide: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
preventOverflow: {
|
||||||
|
boundariesElement: "scrollParent",
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ TransitionProps, placement }) => (
|
||||||
|
<Grow
|
||||||
|
{...TransitionProps}
|
||||||
|
style={{
|
||||||
|
transformOrigin:
|
||||||
|
placement === "bottom" ? "right top" : "right bottom"
|
||||||
|
}}
|
||||||
|
>
|
||||||
<FilterContent
|
<FilterContent
|
||||||
currencySymbol={currencySymbol}
|
currencySymbol={currencySymbol}
|
||||||
filters={menu}
|
filters={data}
|
||||||
onSubmit={data => {
|
onClear={reset}
|
||||||
|
onFilterPropertyChange={dispatch}
|
||||||
|
onSubmit={() => {
|
||||||
onFilterAdd(data);
|
onFilterAdd(data);
|
||||||
setFilterMenuOpened(false);
|
setFilterMenuOpened(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Grow>
|
||||||
</Grow>
|
)}
|
||||||
)}
|
</Popper>
|
||||||
</Popper>
|
</div>
|
||||||
</div>
|
</ClickAwayListener>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Filter.displayName = "Filter";
|
Filter.displayName = "Filter";
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
import TextField, { TextFieldProps } from "@material-ui/core/TextField";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { FilterContentSubmitData, IFilter } from "../Filter";
|
|
||||||
import Filter from "./Filter";
|
|
||||||
|
|
||||||
const useInputStyles = makeStyles(
|
|
||||||
{
|
|
||||||
input: {
|
|
||||||
padding: "10.5px 12px"
|
|
||||||
},
|
|
||||||
root: {
|
|
||||||
flex: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ name: "FilterActions" }
|
|
||||||
);
|
|
||||||
|
|
||||||
const Search: React.FC<TextFieldProps> = props => {
|
|
||||||
const classes = useInputStyles({});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextField
|
|
||||||
{...props}
|
|
||||||
className={classes.root}
|
|
||||||
inputProps={{
|
|
||||||
className: classes.input
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
|
||||||
theme => ({
|
|
||||||
actionContainer: {
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
padding: theme.spacing(1, 3)
|
|
||||||
},
|
|
||||||
searchOnly: {
|
|
||||||
paddingBottom: theme.spacing(1.5)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: "FilterActions"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface FilterActionsPropsSearch {
|
|
||||||
placeholder: string;
|
|
||||||
search: string;
|
|
||||||
onSearchChange: (event: React.ChangeEvent<any>) => void;
|
|
||||||
}
|
|
||||||
export interface FilterActionsPropsFilters<TKeys = string> {
|
|
||||||
currencySymbol: string;
|
|
||||||
menu: IFilter<TKeys>;
|
|
||||||
filterLabel: string;
|
|
||||||
onFilterAdd: (filter: FilterContentSubmitData<TKeys>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FilterActionsOnlySearch: React.FC<
|
|
||||||
FilterActionsPropsSearch
|
|
||||||
> = props => {
|
|
||||||
const { onSearchChange, placeholder, search } = props;
|
|
||||||
const classes = useStyles(props);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(classes.actionContainer, classes.searchOnly)}>
|
|
||||||
<Search
|
|
||||||
fullWidth
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={search}
|
|
||||||
onChange={onSearchChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FilterActionsProps = FilterActionsPropsSearch &
|
|
||||||
FilterActionsPropsFilters;
|
|
||||||
const FilterActions: React.FC<FilterActionsProps> = props => {
|
|
||||||
const {
|
|
||||||
currencySymbol,
|
|
||||||
filterLabel,
|
|
||||||
menu,
|
|
||||||
onFilterAdd,
|
|
||||||
onSearchChange,
|
|
||||||
placeholder,
|
|
||||||
search
|
|
||||||
} = props;
|
|
||||||
const classes = useStyles(props);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.actionContainer}>
|
|
||||||
<Filter
|
|
||||||
currencySymbol={currencySymbol}
|
|
||||||
menu={menu}
|
|
||||||
filterLabel={filterLabel}
|
|
||||||
onFilterAdd={onFilterAdd}
|
|
||||||
/>
|
|
||||||
<Search
|
|
||||||
fullWidth
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={search}
|
|
||||||
onChange={onSearchChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
FilterActions.displayName = "FilterActions";
|
|
||||||
export default FilterActions;
|
|
154
src/components/Filter/FilterAutocompleteField.tsx
Normal file
154
src/components/Filter/FilterAutocompleteField.tsx
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
import React from "react";
|
||||||
|
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import TextField from "@material-ui/core/TextField";
|
||||||
|
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import { toggle } from "@saleor/utils/lists";
|
||||||
|
import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField";
|
||||||
|
import Link from "../Link";
|
||||||
|
import Checkbox from "../Checkbox";
|
||||||
|
import Hr from "../Hr";
|
||||||
|
import { FilterBaseFieldProps } from "./types";
|
||||||
|
|
||||||
|
interface FilterAutocompleteFieldProps extends FilterBaseFieldProps {
|
||||||
|
displayValues: Record<string, MultiAutocompleteChoiceType[]>;
|
||||||
|
setDisplayValues: (
|
||||||
|
values: Record<string, MultiAutocompleteChoiceType[]>
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
hr: {
|
||||||
|
backgroundColor: theme.palette.primary.light,
|
||||||
|
margin: theme.spacing(1, 0)
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
padding: "12px 0 9px 12px"
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
marginBottom: theme.spacing(1)
|
||||||
|
},
|
||||||
|
noResults: {
|
||||||
|
marginTop: theme.spacing(1)
|
||||||
|
},
|
||||||
|
option: {
|
||||||
|
left: -theme.spacing(0.5),
|
||||||
|
position: "relative"
|
||||||
|
},
|
||||||
|
showMore: {
|
||||||
|
display: "inline-block",
|
||||||
|
marginTop: theme.spacing(1)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ name: "FilterAutocompleteField" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const FilterAutocompleteField: React.FC<FilterAutocompleteFieldProps> = ({
|
||||||
|
displayValues,
|
||||||
|
filterField,
|
||||||
|
setDisplayValues,
|
||||||
|
onFilterPropertyChange
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles({});
|
||||||
|
|
||||||
|
const fieldDisplayValues = displayValues[filterField.name];
|
||||||
|
const availableOptions = filterField.options.filter(option =>
|
||||||
|
fieldDisplayValues.every(
|
||||||
|
displayValue => displayValue.value !== option.value
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const displayNoResults =
|
||||||
|
availableOptions.length === 0 && fieldDisplayValues.length === 0;
|
||||||
|
const displayHr = !(
|
||||||
|
(fieldDisplayValues.length === 0 && availableOptions.length > 0) ||
|
||||||
|
(availableOptions.length === 0 && fieldDisplayValues.length > 0) ||
|
||||||
|
displayNoResults
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = (option: MultiAutocompleteChoiceType) => {
|
||||||
|
onFilterPropertyChange({
|
||||||
|
payload: {
|
||||||
|
name: filterField.name,
|
||||||
|
update: {
|
||||||
|
value: toggle(option.value, filterField.value, (a, b) => a === b)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: "set-property"
|
||||||
|
});
|
||||||
|
|
||||||
|
setDisplayValues({
|
||||||
|
...displayValues,
|
||||||
|
[filterField.name]: toggle(
|
||||||
|
option,
|
||||||
|
fieldDisplayValues,
|
||||||
|
(a, b) => a.value === b.value
|
||||||
|
)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
className={classes.inputContainer}
|
||||||
|
fullWidth
|
||||||
|
name={filterField.name + "_autocomplete"}
|
||||||
|
InputProps={{
|
||||||
|
classes: {
|
||||||
|
input: classes.input
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={event => filterField.onSearchChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
{fieldDisplayValues.map(displayValue => (
|
||||||
|
<div className={classes.option} key={displayValue.value}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={filterField.value.includes(displayValue.value)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={displayValue.label}
|
||||||
|
name={filterField.name}
|
||||||
|
onChange={() => handleChange(displayValue)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{displayHr && <Hr className={classes.hr} />}
|
||||||
|
{displayNoResults && (
|
||||||
|
<Typography className={classes.noResults} color="textSecondary">
|
||||||
|
<FormattedMessage defaultMessage="No results" description="search" />
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{availableOptions.map(option => (
|
||||||
|
<div className={classes.option} key={option.value}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox checked={filterField.value.includes(option.value)} />
|
||||||
|
}
|
||||||
|
label={option.label}
|
||||||
|
name={filterField.name}
|
||||||
|
onChange={() => handleChange(option)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{filterField.hasMore && (
|
||||||
|
<Link
|
||||||
|
className={classes.showMore}
|
||||||
|
underline
|
||||||
|
onClick={filterField.onFetchMore}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Show more"
|
||||||
|
description="search results"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FilterAutocompleteField.displayName = "FilterAutocompleteField";
|
||||||
|
export default FilterAutocompleteField;
|
|
@ -1,147 +1,395 @@
|
||||||
import Button from "@material-ui/core/Button";
|
import Button from "@material-ui/core/Button";
|
||||||
|
import Paper from "@material-ui/core/Paper";
|
||||||
|
import Radio from "@material-ui/core/Radio";
|
||||||
import Typography from "@material-ui/core/Typography";
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl, IntlShape } from "react-intl";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||||
import { getMenuItemByValue, isLeaf, walkToRoot } from "../../utils/menu";
|
import { fade } from "@material-ui/core/styles/colorManipulator";
|
||||||
import FormSpacer from "../FormSpacer";
|
import { buttonMessages } from "@saleor/intl";
|
||||||
|
import { TextField } from "@material-ui/core";
|
||||||
|
import useStateFromProps from "@saleor/hooks/useStateFromProps";
|
||||||
|
import Hr from "../Hr";
|
||||||
|
import Checkbox from "../Checkbox";
|
||||||
import SingleSelectField from "../SingleSelectField";
|
import SingleSelectField from "../SingleSelectField";
|
||||||
import FilterElement from "./FilterElement";
|
import { SingleAutocompleteChoiceType } from "../SingleAutocompleteSelectField";
|
||||||
import { IFilter } from "./types";
|
import FormSpacer from "../FormSpacer";
|
||||||
|
import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField";
|
||||||
|
import { IFilter, FieldType, FilterType } from "./types";
|
||||||
|
import Arrow from "./Arrow";
|
||||||
|
import { FilterReducerAction } from "./reducer";
|
||||||
|
import FilterAutocompleteField from "./FilterAutocompleteField";
|
||||||
|
import FilterOptionField from "./FilterOptionField";
|
||||||
|
|
||||||
export interface FilterContentSubmitData<TKeys = string> {
|
export interface FilterContentProps<T extends string = string> {
|
||||||
name: TKeys;
|
|
||||||
value: string | string[];
|
|
||||||
}
|
|
||||||
export interface FilterContentProps {
|
|
||||||
currencySymbol: string;
|
currencySymbol: string;
|
||||||
filters: IFilter<string>;
|
filters: IFilter<T>;
|
||||||
onSubmit: (data: FilterContentSubmitData) => void;
|
onFilterPropertyChange: React.Dispatch<FilterReducerAction<T>>;
|
||||||
}
|
onClear: () => void;
|
||||||
|
onSubmit: () => void;
|
||||||
function checkFilterValue(value: string | string[]): boolean {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return !!value;
|
|
||||||
}
|
|
||||||
return value.some(v => !!v);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFilterChoices(items: IFilter<string>) {
|
|
||||||
return items.map(filterItem => ({
|
|
||||||
label: filterItem.label,
|
|
||||||
value: filterItem.value.toString()
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
{
|
theme => ({
|
||||||
|
actionBar: {
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: theme.spacing(1, 3)
|
||||||
|
},
|
||||||
|
andLabel: {
|
||||||
|
margin: theme.spacing(0, 2)
|
||||||
|
},
|
||||||
|
arrow: {
|
||||||
|
marginRight: theme.spacing(2)
|
||||||
|
},
|
||||||
|
clear: {
|
||||||
|
marginRight: theme.spacing(1)
|
||||||
|
},
|
||||||
|
filterFieldBar: {
|
||||||
|
"&:not(:last-of-type)": {
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`
|
||||||
|
},
|
||||||
|
padding: theme.spacing(1, 2.5)
|
||||||
|
},
|
||||||
|
filterSettings: {
|
||||||
|
background: fade(theme.palette.primary.main, 0.2),
|
||||||
|
padding: theme.spacing(2, 3)
|
||||||
|
},
|
||||||
input: {
|
input: {
|
||||||
padding: "20px 12px 17px"
|
padding: "12px 0 9px 12px"
|
||||||
|
},
|
||||||
|
inputRange: {
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex"
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontWeight: 600
|
||||||
|
},
|
||||||
|
option: {
|
||||||
|
left: -theme.spacing(0.5),
|
||||||
|
position: "relative"
|
||||||
|
},
|
||||||
|
optionRadio: {
|
||||||
|
left: -theme.spacing(0.25)
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
{ name: "FilterContent" }
|
{ name: "FilterContent" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function getIsFilterMultipleChoices(
|
||||||
|
intl: IntlShape
|
||||||
|
): SingleAutocompleteChoiceType[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
defaultMessage: "equal to",
|
||||||
|
description: "is filter range or value"
|
||||||
|
}),
|
||||||
|
value: FilterType.SINGULAR
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
defaultMessage: "between",
|
||||||
|
description: "is filter range or value"
|
||||||
|
}),
|
||||||
|
value: FilterType.MULTIPLE
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const FilterContent: React.FC<FilterContentProps> = ({
|
const FilterContent: React.FC<FilterContentProps> = ({
|
||||||
currencySymbol,
|
currencySymbol,
|
||||||
filters,
|
filters,
|
||||||
|
onClear,
|
||||||
|
onFilterPropertyChange,
|
||||||
onSubmit
|
onSubmit
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [menuValue, setMenuValue] = React.useState<string>(null);
|
|
||||||
const [filterValue, setFilterValue] = React.useState<string | string[]>("");
|
|
||||||
const classes = useStyles({});
|
const classes = useStyles({});
|
||||||
|
const [
|
||||||
|
autocompleteDisplayValues,
|
||||||
|
setAutocompleteDisplayValues
|
||||||
|
] = useStateFromProps<Record<string, MultiAutocompleteChoiceType[]>>(
|
||||||
|
filters.reduce((acc, filterField) => {
|
||||||
|
if (filterField.type === FieldType.autocomplete) {
|
||||||
|
acc[filterField.name] = filterField.displayValues;
|
||||||
|
}
|
||||||
|
|
||||||
const activeMenu = menuValue
|
return acc;
|
||||||
? getMenuItemByValue(filters, menuValue)
|
}, {})
|
||||||
: undefined;
|
);
|
||||||
const menus = menuValue
|
|
||||||
? walkToRoot(filters, menuValue).slice(-1)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const onMenuChange = (event: React.ChangeEvent<any>) => {
|
|
||||||
setMenuValue(event.target.value);
|
|
||||||
setFilterValue("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Paper>
|
||||||
<SingleSelectField
|
<form
|
||||||
choices={getFilterChoices(filters)}
|
onSubmit={event => {
|
||||||
onChange={onMenuChange}
|
event.preventDefault();
|
||||||
selectProps={{
|
onSubmit();
|
||||||
classes: {
|
|
||||||
selectMenu: classes.input
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
value={menus ? menus[0].value : menuValue}
|
>
|
||||||
placeholder={intl.formatMessage({
|
<div className={classes.actionBar}>
|
||||||
defaultMessage: "Select Filter..."
|
<Typography className={classes.label}>
|
||||||
})}
|
<FormattedMessage defaultMessage="Filters" />
|
||||||
/>
|
</Typography>
|
||||||
{menus &&
|
<div>
|
||||||
menus.map(
|
<Button className={classes.clear} onClick={onClear}>
|
||||||
(filterItem, filterItemIndex) =>
|
<FormattedMessage {...buttonMessages.clear} />
|
||||||
!isLeaf(filterItem) && (
|
</Button>
|
||||||
<React.Fragment
|
<Button color="primary" variant="contained" type="submit">
|
||||||
key={filterItem.label.toString() + ":" + filterItem.value}
|
<FormattedMessage {...buttonMessages.done} />
|
||||||
>
|
</Button>
|
||||||
<FormSpacer />
|
</div>
|
||||||
<SingleSelectField
|
</div>
|
||||||
choices={getFilterChoices(filterItem.children)}
|
<Hr />
|
||||||
onChange={onMenuChange}
|
{filters
|
||||||
selectProps={{
|
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||||
classes: {
|
.map(filterField => (
|
||||||
selectMenu: classes.input
|
<React.Fragment key={filterField.name}>
|
||||||
}
|
<div className={classes.filterFieldBar}>
|
||||||
}}
|
<FormControlLabel
|
||||||
value={
|
control={<Checkbox checked={filterField.active} />}
|
||||||
filterItemIndex === menus.length - 1
|
label={filterField.label}
|
||||||
? menuValue.toString()
|
onChange={() =>
|
||||||
: menus[filterItemIndex - 1].label.toString()
|
onFilterPropertyChange({
|
||||||
|
payload: {
|
||||||
|
name: filterField.name,
|
||||||
|
update: {
|
||||||
|
active: !filterField.active
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: "set-property"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
placeholder={intl.formatMessage({
|
|
||||||
defaultMessage: "Select Filter..."
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</div>
|
||||||
)
|
{filterField.active && (
|
||||||
)}
|
<div className={classes.filterSettings}>
|
||||||
{activeMenu && isLeaf(activeMenu) && (
|
{filterField.type === FieldType.text && (
|
||||||
<>
|
<TextField
|
||||||
<FormSpacer />
|
fullWidth
|
||||||
{activeMenu.data.additionalText && (
|
name={filterField.name}
|
||||||
<Typography>{activeMenu.data.additionalText}</Typography>
|
InputProps={{
|
||||||
)}
|
classes: {
|
||||||
<FilterElement
|
input: classes.input
|
||||||
currencySymbol={currencySymbol}
|
}
|
||||||
filter={activeMenu}
|
}}
|
||||||
value={filterValue}
|
value={filterField.value[0]}
|
||||||
onChange={value => setFilterValue(value)}
|
onChange={event =>
|
||||||
/>
|
onFilterPropertyChange({
|
||||||
{checkFilterValue(filterValue) && (
|
payload: {
|
||||||
<>
|
name: filterField.name,
|
||||||
<FormSpacer />
|
update: {
|
||||||
<Button
|
value: [event.target.value, filterField.value[1]]
|
||||||
color="primary"
|
}
|
||||||
onClick={() =>
|
},
|
||||||
onSubmit({
|
type: "set-property"
|
||||||
name: activeMenu.value,
|
})
|
||||||
value: filterValue
|
}
|
||||||
})
|
/>
|
||||||
}
|
)}
|
||||||
>
|
{[FieldType.date, FieldType.price, FieldType.number].includes(
|
||||||
<FormattedMessage
|
filterField.type
|
||||||
defaultMessage="Add filter"
|
) && (
|
||||||
description="button"
|
<>
|
||||||
/>
|
<SingleSelectField
|
||||||
</Button>
|
choices={getIsFilterMultipleChoices(intl)}
|
||||||
</>
|
value={
|
||||||
)}
|
filterField.multiple
|
||||||
</>
|
? FilterType.MULTIPLE
|
||||||
)}
|
: FilterType.SINGULAR
|
||||||
</>
|
}
|
||||||
|
InputProps={{
|
||||||
|
classes: {
|
||||||
|
input: classes.input
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={event =>
|
||||||
|
onFilterPropertyChange({
|
||||||
|
payload: {
|
||||||
|
name: filterField.name,
|
||||||
|
update: {
|
||||||
|
multiple:
|
||||||
|
event.target.value === FilterType.MULTIPLE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: "set-property"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FormSpacer />
|
||||||
|
<div className={classes.inputRange}>
|
||||||
|
<div>
|
||||||
|
<Arrow className={classes.arrow} />
|
||||||
|
</div>
|
||||||
|
{filterField.multiple ? (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name={filterField.name + "_min"}
|
||||||
|
InputProps={{
|
||||||
|
classes: {
|
||||||
|
input: classes.input
|
||||||
|
},
|
||||||
|
endAdornment:
|
||||||
|
filterField.type === FieldType.price &&
|
||||||
|
currencySymbol,
|
||||||
|
type:
|
||||||
|
filterField.type === FieldType.date
|
||||||
|
? "date"
|
||||||
|
: "number"
|
||||||
|
}}
|
||||||
|
value={filterField.value[0]}
|
||||||
|
onChange={event =>
|
||||||
|
onFilterPropertyChange({
|
||||||
|
payload: {
|
||||||
|
name: filterField.name,
|
||||||
|
update: {
|
||||||
|
value: [
|
||||||
|
event.target.value,
|
||||||
|
filterField.value[1]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: "set-property"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className={classes.andLabel}>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="and"
|
||||||
|
description="filter range separator"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name={filterField.name + "_max"}
|
||||||
|
InputProps={{
|
||||||
|
classes: {
|
||||||
|
input: classes.input
|
||||||
|
},
|
||||||
|
endAdornment:
|
||||||
|
filterField.type === FieldType.price &&
|
||||||
|
currencySymbol,
|
||||||
|
type:
|
||||||
|
filterField.type === FieldType.date
|
||||||
|
? "date"
|
||||||
|
: "number"
|
||||||
|
}}
|
||||||
|
value={filterField.value[1]}
|
||||||
|
onChange={event =>
|
||||||
|
onFilterPropertyChange({
|
||||||
|
payload: {
|
||||||
|
name: filterField.name,
|
||||||
|
update: {
|
||||||
|
value: [
|
||||||
|
filterField.value[0],
|
||||||
|
event.target.value
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: "set-property"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name={filterField.name}
|
||||||
|
InputProps={{
|
||||||
|
classes: {
|
||||||
|
input: classes.input
|
||||||
|
},
|
||||||
|
endAdornment:
|
||||||
|
filterField.type === FieldType.price &&
|
||||||
|
currencySymbol,
|
||||||
|
type:
|
||||||
|
filterField.type === FieldType.date
|
||||||
|
? "date"
|
||||||
|
: [
|
||||||
|
FieldType.number,
|
||||||
|
FieldType.price
|
||||||
|
].includes(filterField.type)
|
||||||
|
? "number"
|
||||||
|
: "text"
|
||||||
|
}}
|
||||||
|
value={filterField.value[0]}
|
||||||
|
onChange={event =>
|
||||||
|
onFilterPropertyChange({
|
||||||
|
payload: {
|
||||||
|
name: filterField.name,
|
||||||
|
update: {
|
||||||
|
value: [
|
||||||
|
event.target.value,
|
||||||
|
filterField.value[1]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: "set-property"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{filterField.type === FieldType.options && (
|
||||||
|
<FilterOptionField
|
||||||
|
filterField={filterField}
|
||||||
|
onFilterPropertyChange={onFilterPropertyChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{filterField.type === FieldType.boolean &&
|
||||||
|
filterField.options.map(option => (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
classes.option,
|
||||||
|
classes.optionRadio
|
||||||
|
)}
|
||||||
|
key={option.value}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Radio
|
||||||
|
checked={filterField.value[0] === option.value}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={option.label}
|
||||||
|
name={filterField.name}
|
||||||
|
onChange={() =>
|
||||||
|
onFilterPropertyChange({
|
||||||
|
payload: {
|
||||||
|
name: filterField.name,
|
||||||
|
update: {
|
||||||
|
value: [option.value]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: "set-property"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{filterField.type === FieldType.autocomplete &&
|
||||||
|
filterField.multiple && (
|
||||||
|
<FilterAutocompleteField
|
||||||
|
displayValues={autocompleteDisplayValues}
|
||||||
|
filterField={filterField}
|
||||||
|
setDisplayValues={setAutocompleteDisplayValues}
|
||||||
|
onFilterPropertyChange={onFilterPropertyChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
FilterContent.displayName = "FilterContent";
|
FilterContent.displayName = "FilterContent";
|
||||||
|
|
|
@ -1,229 +0,0 @@
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
import TextField from "@material-ui/core/TextField";
|
|
||||||
import Typography from "@material-ui/core/Typography";
|
|
||||||
import React from "react";
|
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
|
||||||
|
|
||||||
import Calendar from "../../icons/Calendar";
|
|
||||||
import FormSpacer from "../FormSpacer";
|
|
||||||
import PriceField from "../PriceField";
|
|
||||||
import SingleSelectField from "../SingleSelectField";
|
|
||||||
import { FieldType, IFilterItem } from "./types";
|
|
||||||
|
|
||||||
export interface FilterElementProps<TFilterKeys = string> {
|
|
||||||
className?: string;
|
|
||||||
filter: IFilterItem<TFilterKeys>;
|
|
||||||
value: string | string[];
|
|
||||||
onChange: (value: string | string[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
|
||||||
{
|
|
||||||
calendar: {
|
|
||||||
margin: 8
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
padding: "20px 12px 17px"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ name: "FilterElement" }
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface FilterElementProps<TFilterKeys = string> {
|
|
||||||
className?: string;
|
|
||||||
currencySymbol: string;
|
|
||||||
filter: IFilterItem<TFilterKeys>;
|
|
||||||
value: string | string[];
|
|
||||||
onChange: (value: string | string[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FilterElement: React.FC<FilterElementProps> = ({
|
|
||||||
currencySymbol,
|
|
||||||
className,
|
|
||||||
filter,
|
|
||||||
onChange,
|
|
||||||
value
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const classes = useStyles({});
|
|
||||||
|
|
||||||
if (filter.data.type === FieldType.date) {
|
|
||||||
return (
|
|
||||||
<TextField
|
|
||||||
className={className}
|
|
||||||
fullWidth
|
|
||||||
type="date"
|
|
||||||
onChange={event => onChange(event.target.value)}
|
|
||||||
value={value}
|
|
||||||
InputProps={{
|
|
||||||
classes: {
|
|
||||||
input: classes.input
|
|
||||||
},
|
|
||||||
startAdornment: <Calendar className={classes.calendar} />
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (filter.data.type === FieldType.rangeDate) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Typography>
|
|
||||||
<FormattedMessage defaultMessage="from" />
|
|
||||||
</Typography>
|
|
||||||
<TextField
|
|
||||||
className={className}
|
|
||||||
fullWidth
|
|
||||||
type="date"
|
|
||||||
value={value[0]}
|
|
||||||
onChange={event => onChange([event.target.value, value[1]])}
|
|
||||||
InputProps={{
|
|
||||||
classes: {
|
|
||||||
input: classes.input
|
|
||||||
},
|
|
||||||
startAdornment: <Calendar className={classes.calendar} />
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormSpacer />
|
|
||||||
<Typography>
|
|
||||||
<FormattedMessage defaultMessage="to" />
|
|
||||||
</Typography>
|
|
||||||
<TextField
|
|
||||||
className={className}
|
|
||||||
fullWidth
|
|
||||||
type="date"
|
|
||||||
value={value[1]}
|
|
||||||
onChange={event => onChange([value[0], event.target.value])}
|
|
||||||
InputProps={{
|
|
||||||
classes: {
|
|
||||||
input: classes.input
|
|
||||||
},
|
|
||||||
startAdornment: <Calendar className={classes.calendar} />
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (filter.data.type === FieldType.range) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Typography>
|
|
||||||
<FormattedMessage defaultMessage="from" />
|
|
||||||
</Typography>
|
|
||||||
<TextField
|
|
||||||
className={className}
|
|
||||||
fullWidth
|
|
||||||
value={value[0]}
|
|
||||||
onChange={event => onChange([event.target.value, value[1]])}
|
|
||||||
type="number"
|
|
||||||
InputProps={{
|
|
||||||
classes: {
|
|
||||||
input: classes.input
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormSpacer />
|
|
||||||
<Typography>
|
|
||||||
<FormattedMessage defaultMessage="to" />
|
|
||||||
</Typography>
|
|
||||||
<TextField
|
|
||||||
className={className}
|
|
||||||
fullWidth
|
|
||||||
value={value[1]}
|
|
||||||
onChange={event => onChange([value[0], event.target.value])}
|
|
||||||
type="number"
|
|
||||||
InputProps={{
|
|
||||||
classes: {
|
|
||||||
input: classes.input
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (filter.data.type === FieldType.rangePrice) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Typography>
|
|
||||||
<FormattedMessage defaultMessage="from" />
|
|
||||||
</Typography>
|
|
||||||
<PriceField
|
|
||||||
currencySymbol={currencySymbol}
|
|
||||||
className={className}
|
|
||||||
value={value[0]}
|
|
||||||
onChange={event => onChange([event.target.value, value[1]])}
|
|
||||||
InputProps={{
|
|
||||||
classes: {
|
|
||||||
input: classes.input
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormSpacer />
|
|
||||||
<Typography>
|
|
||||||
<FormattedMessage defaultMessage="to" />
|
|
||||||
</Typography>
|
|
||||||
<PriceField
|
|
||||||
currencySymbol={currencySymbol}
|
|
||||||
className={className}
|
|
||||||
value={value[1]}
|
|
||||||
onChange={event => onChange([value[0], event.target.value])}
|
|
||||||
InputProps={{
|
|
||||||
classes: {
|
|
||||||
input: classes.input
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (filter.data.type === FieldType.select) {
|
|
||||||
return (
|
|
||||||
<SingleSelectField
|
|
||||||
choices={filter.data.options.map(option => ({
|
|
||||||
...option,
|
|
||||||
value: option.value.toString()
|
|
||||||
}))}
|
|
||||||
selectProps={{
|
|
||||||
className,
|
|
||||||
inputProps: {
|
|
||||||
className: classes.input
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
value={value as string}
|
|
||||||
placeholder={intl.formatMessage({
|
|
||||||
defaultMessage: "Select Filter..."
|
|
||||||
})}
|
|
||||||
onChange={event => onChange(event.target.value)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (filter.data.type === FieldType.price) {
|
|
||||||
return (
|
|
||||||
<PriceField
|
|
||||||
currencySymbol={currencySymbol}
|
|
||||||
className={className}
|
|
||||||
label={filter.data.fieldLabel}
|
|
||||||
onChange={event => onChange(event.target.value)}
|
|
||||||
InputProps={{
|
|
||||||
classes: {
|
|
||||||
input: !filter.data.fieldLabel && classes.input
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
value={value as string}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (filter.data.type === FieldType.hidden) {
|
|
||||||
onChange(filter.data.value);
|
|
||||||
return <input type="hidden" value={value} />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<TextField
|
|
||||||
className={className}
|
|
||||||
fullWidth
|
|
||||||
label={filter.data.fieldLabel}
|
|
||||||
InputProps={{
|
|
||||||
classes: {
|
|
||||||
input: !filter.data.fieldLabel && classes.input
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={event => onChange(event.target.value)}
|
|
||||||
value={value as string}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
FilterElement.displayName = "FilterElement";
|
|
||||||
export default FilterElement;
|
|
74
src/components/Filter/FilterOptionField.tsx
Normal file
74
src/components/Filter/FilterOptionField.tsx
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||||
|
import Radio from "@material-ui/core/Radio";
|
||||||
|
import React from "react";
|
||||||
|
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import { toggle } from "@saleor/utils/lists";
|
||||||
|
import Checkbox from "../Checkbox";
|
||||||
|
import { FilterBaseFieldProps } from "./types";
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
option: {
|
||||||
|
left: -theme.spacing(0.5),
|
||||||
|
position: "relative"
|
||||||
|
},
|
||||||
|
optionRadio: {
|
||||||
|
left: -theme.spacing(0.25)
|
||||||
|
},
|
||||||
|
root: {}
|
||||||
|
}),
|
||||||
|
{ name: "FilterOptionField" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const FilterOptionField: React.FC<FilterBaseFieldProps> = ({
|
||||||
|
filterField,
|
||||||
|
onFilterPropertyChange
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles({});
|
||||||
|
const handleSelect = (value: string) =>
|
||||||
|
onFilterPropertyChange({
|
||||||
|
payload: {
|
||||||
|
name: filterField.name,
|
||||||
|
update: {
|
||||||
|
value: filterField.multiple
|
||||||
|
? toggle(value, filterField.value, (a, b) => a === b)
|
||||||
|
: [value]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: "set-property"
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
{filterField.options.map(option => (
|
||||||
|
<div
|
||||||
|
className={classNames(classes.option, {
|
||||||
|
[classes.optionRadio]: !filterField.multiple
|
||||||
|
})}
|
||||||
|
key={option.value}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
filterField.multiple ? (
|
||||||
|
<Checkbox checked={filterField.value.includes(option.value)} />
|
||||||
|
) : (
|
||||||
|
<Radio
|
||||||
|
checked={filterField.value[0] === option.value}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
label={option.label}
|
||||||
|
name={filterField.name}
|
||||||
|
onChange={() => handleSelect(option.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FilterOptionField.displayName = "FilterOptionField";
|
||||||
|
export default FilterOptionField;
|
|
@ -1,97 +0,0 @@
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
import React from "react";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
|
|
||||||
import { SearchPageProps } from "../../types";
|
|
||||||
import Debounce from "../Debounce";
|
|
||||||
import { FilterActionsOnlySearch } from "../Filter/FilterActions";
|
|
||||||
import Hr from "../Hr";
|
|
||||||
import Link from "../Link";
|
|
||||||
|
|
||||||
export interface FilterSearchProps extends SearchPageProps {
|
|
||||||
displaySearchAction: "save" | "delete" | null;
|
|
||||||
searchPlaceholder: string;
|
|
||||||
onSearchDelete?: () => void;
|
|
||||||
onSearchSave?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
|
||||||
theme => ({
|
|
||||||
tabAction: {
|
|
||||||
display: "inline-block"
|
|
||||||
},
|
|
||||||
tabActionContainer: {
|
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
marginTop: theme.spacing(),
|
|
||||||
padding: theme.spacing(0, 1, 3, 1)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: "FilterSearch"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const FilterSearch: React.FC<FilterSearchProps> = props => {
|
|
||||||
const {
|
|
||||||
displaySearchAction,
|
|
||||||
initialSearch,
|
|
||||||
onSearchChange,
|
|
||||||
onSearchDelete,
|
|
||||||
onSearchSave,
|
|
||||||
searchPlaceholder
|
|
||||||
} = props;
|
|
||||||
const classes = useStyles(props);
|
|
||||||
const [search, setSearch] = React.useState(initialSearch);
|
|
||||||
React.useEffect(() => setSearch(initialSearch), [initialSearch]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Debounce debounceFn={onSearchChange}>
|
|
||||||
{debounceSearchChange => {
|
|
||||||
const handleSearchChange = (event: React.ChangeEvent<any>) => {
|
|
||||||
const value = event.target.value;
|
|
||||||
setSearch(value);
|
|
||||||
debounceSearchChange(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FilterActionsOnlySearch
|
|
||||||
{...props}
|
|
||||||
placeholder={searchPlaceholder}
|
|
||||||
search={search}
|
|
||||||
onSearchChange={handleSearchChange}
|
|
||||||
/>
|
|
||||||
{!!displaySearchAction ? (
|
|
||||||
<div className={classes.tabActionContainer}>
|
|
||||||
<div className={classes.tabAction}>
|
|
||||||
{displaySearchAction === "save" ? (
|
|
||||||
<Link onClick={onSearchSave}>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Save Custom Search"
|
|
||||||
description="button"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Link onClick={onSearchDelete}>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Delete Search"
|
|
||||||
description="button"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Hr />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Debounce>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
FilterSearch.displayName = "FilterSearch";
|
|
||||||
export default FilterSearch;
|
|
69
src/components/Filter/reducer.ts
Normal file
69
src/components/Filter/reducer.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import { update } from "@saleor/utils/lists";
|
||||||
|
import { IFilter, IFilterElementMutableData } from "./types";
|
||||||
|
|
||||||
|
export type FilterReducerActionType =
|
||||||
|
| "clear"
|
||||||
|
| "merge"
|
||||||
|
| "reset"
|
||||||
|
| "set-property";
|
||||||
|
export interface FilterReducerAction<T extends string> {
|
||||||
|
type: FilterReducerActionType;
|
||||||
|
payload: Partial<{
|
||||||
|
name: T;
|
||||||
|
update: Partial<IFilterElementMutableData>;
|
||||||
|
new: IFilter<T>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge<T extends string>(
|
||||||
|
prevState: IFilter<T>,
|
||||||
|
newState: IFilter<T>
|
||||||
|
): IFilter<T> {
|
||||||
|
return newState.map(newFilter => {
|
||||||
|
const prevFilter = prevState.find(
|
||||||
|
prevFilter => prevFilter.name === newFilter.name
|
||||||
|
);
|
||||||
|
if (!!prevFilter) {
|
||||||
|
return {
|
||||||
|
...newFilter,
|
||||||
|
active: prevFilter.active,
|
||||||
|
value: prevFilter.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFilter;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProperty<T extends string>(
|
||||||
|
prevState: IFilter<T>,
|
||||||
|
filter: T,
|
||||||
|
updateData: Partial<IFilterElementMutableData>
|
||||||
|
): IFilter<T> {
|
||||||
|
const field = prevState.find(f => f.name === filter);
|
||||||
|
const updatedField = {
|
||||||
|
...field,
|
||||||
|
...updateData
|
||||||
|
};
|
||||||
|
|
||||||
|
return update(updatedField, prevState, (a, b) => a.name === b.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reduceFilter<T extends string>(
|
||||||
|
prevState: IFilter<T>,
|
||||||
|
action: FilterReducerAction<T>
|
||||||
|
): IFilter<T> {
|
||||||
|
switch (action.type) {
|
||||||
|
case "set-property":
|
||||||
|
return setProperty(prevState, action.payload.name, action.payload.update);
|
||||||
|
case "merge":
|
||||||
|
return merge(prevState, action.payload.new);
|
||||||
|
case "reset":
|
||||||
|
return action.payload.new;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return prevState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default reduceFilter;
|
|
@ -1,30 +1,43 @@
|
||||||
import { IMenu, IMenuItem } from "../../utils/menu";
|
import { FetchMoreProps, SearchPageProps } from "@saleor/types";
|
||||||
|
import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField";
|
||||||
|
import { FilterReducerAction } from "./reducer";
|
||||||
|
|
||||||
export enum FieldType {
|
export enum FieldType {
|
||||||
|
autocomplete,
|
||||||
|
boolean,
|
||||||
date,
|
date,
|
||||||
hidden,
|
dateTime,
|
||||||
number,
|
number,
|
||||||
price,
|
price,
|
||||||
range,
|
options,
|
||||||
rangeDate,
|
|
||||||
rangePrice,
|
|
||||||
select,
|
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FilterChoice {
|
export interface IFilterElementMutableData<T extends string = string> {
|
||||||
|
active: boolean;
|
||||||
|
multiple: boolean;
|
||||||
|
options?: MultiAutocompleteChoiceType[];
|
||||||
|
value: T[];
|
||||||
|
}
|
||||||
|
export interface IFilterElement<T extends string = string>
|
||||||
|
extends IFilterElementMutableData,
|
||||||
|
Partial<FetchMoreProps & SearchPageProps> {
|
||||||
|
autocomplete?: boolean;
|
||||||
|
displayValues?: MultiAutocompleteChoiceType[];
|
||||||
|
group?: T;
|
||||||
label: string;
|
label: string;
|
||||||
value: string | boolean;
|
name: T;
|
||||||
}
|
|
||||||
|
|
||||||
export interface FilterData {
|
|
||||||
additionalText?: string;
|
|
||||||
fieldLabel: string;
|
|
||||||
options?: FilterChoice[];
|
|
||||||
type: FieldType;
|
type: FieldType;
|
||||||
value?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IFilterItem<TKeys> = IMenuItem<FilterData, TKeys>;
|
export interface FilterBaseFieldProps<T extends string = string> {
|
||||||
|
filterField: IFilterElement<T>;
|
||||||
|
onFilterPropertyChange: React.Dispatch<FilterReducerAction<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
export type IFilter<TKeys> = IMenu<FilterData, TKeys>;
|
export type IFilter<T extends string = string> = Array<IFilterElement<T>>;
|
||||||
|
|
||||||
|
export enum FilterType {
|
||||||
|
MULTIPLE = "MULTIPLE",
|
||||||
|
SINGULAR = "SINGULAR"
|
||||||
|
}
|
||||||
|
|
38
src/components/Filter/useFilter.ts
Normal file
38
src/components/Filter/useFilter.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { useReducer, useEffect, Dispatch } from "react";
|
||||||
|
|
||||||
|
import reduceFilter, { FilterReducerAction } from "./reducer";
|
||||||
|
import { IFilter, IFilterElement } from "./types";
|
||||||
|
|
||||||
|
export type UseFilter<T extends string> = [
|
||||||
|
Array<IFilterElement<T>>,
|
||||||
|
Dispatch<FilterReducerAction<T>>,
|
||||||
|
() => void
|
||||||
|
];
|
||||||
|
|
||||||
|
function useFilter<T extends string>(initialFilter: IFilter<T>): UseFilter<T> {
|
||||||
|
const [data, dispatchFilterAction] = useReducer<
|
||||||
|
React.Reducer<IFilter<T>, FilterReducerAction<T>>
|
||||||
|
>(reduceFilter, initialFilter);
|
||||||
|
|
||||||
|
const reset = () =>
|
||||||
|
dispatchFilterAction({
|
||||||
|
payload: {
|
||||||
|
new: initialFilter
|
||||||
|
},
|
||||||
|
type: "reset"
|
||||||
|
});
|
||||||
|
|
||||||
|
const refresh = () =>
|
||||||
|
dispatchFilterAction({
|
||||||
|
payload: {
|
||||||
|
new: initialFilter
|
||||||
|
},
|
||||||
|
type: "merge"
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(refresh, [initialFilter]);
|
||||||
|
|
||||||
|
return [data, dispatchFilterAction, reset];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useFilter;
|
|
@ -1,37 +1,66 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||||
|
import Button from "@material-ui/core/Button";
|
||||||
|
|
||||||
import { FilterProps } from "../../types";
|
import { FilterProps } from "../../types";
|
||||||
import Debounce from "../Debounce";
|
|
||||||
import { IFilter } from "../Filter/types";
|
import { IFilter } from "../Filter/types";
|
||||||
import FilterTabs, { FilterChips, FilterTab } from "../TableFilter";
|
import FilterTabs, { FilterTab } from "../TableFilter";
|
||||||
|
import { SearchBarProps } from "../SearchBar";
|
||||||
|
import SearchInput from "../SearchBar/SearchInput";
|
||||||
|
import Filter from "../Filter";
|
||||||
|
|
||||||
export interface FilterBarProps<TKeys = string> extends FilterProps {
|
export interface FilterBarProps<TKeys extends string = string>
|
||||||
filterMenu: IFilter<TKeys>;
|
extends FilterProps<TKeys>,
|
||||||
|
SearchBarProps {
|
||||||
|
filterStructure: IFilter<TKeys>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilterBar: React.FC<FilterBarProps> = ({
|
const useStyles = makeStyles(
|
||||||
allTabLabel,
|
theme => ({
|
||||||
currencySymbol,
|
root: {
|
||||||
filterLabel,
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
filtersList,
|
display: "flex",
|
||||||
filterMenu,
|
flexWrap: "wrap",
|
||||||
currentTab,
|
padding: theme.spacing(1, 3)
|
||||||
initialSearch,
|
},
|
||||||
searchPlaceholder,
|
tabActionButton: {
|
||||||
tabs,
|
marginLeft: theme.spacing(2),
|
||||||
onAll,
|
paddingLeft: theme.spacing(3),
|
||||||
onSearchChange,
|
paddingRight: theme.spacing(3)
|
||||||
onFilterAdd,
|
}
|
||||||
onTabChange,
|
}),
|
||||||
onTabDelete,
|
{
|
||||||
onTabSave
|
name: "FilterBar"
|
||||||
}) => {
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const FilterBar: React.FC<FilterBarProps> = props => {
|
||||||
|
const {
|
||||||
|
allTabLabel,
|
||||||
|
currencySymbol,
|
||||||
|
filterStructure,
|
||||||
|
currentTab,
|
||||||
|
initialSearch,
|
||||||
|
searchPlaceholder,
|
||||||
|
tabs,
|
||||||
|
onAll,
|
||||||
|
onSearchChange,
|
||||||
|
onFilterChange,
|
||||||
|
onTabChange,
|
||||||
|
onTabDelete,
|
||||||
|
onTabSave
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const classes = useStyles(props);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [search, setSearch] = React.useState(initialSearch);
|
|
||||||
React.useEffect(() => setSearch(initialSearch), [currentTab, initialSearch]);
|
|
||||||
|
|
||||||
const isCustom = currentTab === tabs.length + 1;
|
const isCustom = currentTab === tabs.length + 1;
|
||||||
|
const displayTabAction = isCustom
|
||||||
|
? "save"
|
||||||
|
: currentTab === 0
|
||||||
|
? null
|
||||||
|
: "delete";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -53,34 +82,44 @@ const FilterBar: React.FC<FilterBarProps> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</FilterTabs>
|
</FilterTabs>
|
||||||
<Debounce debounceFn={onSearchChange}>
|
<div className={classes.root}>
|
||||||
{debounceSearchChange => {
|
<Filter
|
||||||
const handleSearchChange = (event: React.ChangeEvent<any>) => {
|
currencySymbol={currencySymbol}
|
||||||
const value = event.target.value;
|
menu={filterStructure}
|
||||||
setSearch(value);
|
onFilterAdd={onFilterChange}
|
||||||
debounceSearchChange(value);
|
/>
|
||||||
};
|
<SearchInput
|
||||||
|
initialSearch={initialSearch}
|
||||||
return (
|
placeholder={searchPlaceholder}
|
||||||
<FilterChips
|
onSearchChange={onSearchChange}
|
||||||
currencySymbol={currencySymbol}
|
/>
|
||||||
displayTabAction={
|
{displayTabAction &&
|
||||||
!!initialSearch ? (isCustom ? "save" : "delete") : null
|
(displayTabAction === "save" ? (
|
||||||
}
|
<Button
|
||||||
menu={filterMenu}
|
className={classes.tabActionButton}
|
||||||
filtersList={filtersList}
|
color="primary"
|
||||||
filterLabel={filterLabel}
|
onClick={onTabSave}
|
||||||
placeholder={searchPlaceholder}
|
>
|
||||||
search={search}
|
<FormattedMessage
|
||||||
onSearchChange={handleSearchChange}
|
defaultMessage="Save Search"
|
||||||
onFilterAdd={onFilterAdd}
|
description="button"
|
||||||
onFilterSave={onTabSave}
|
/>
|
||||||
isCustomSearch={isCustom}
|
</Button>
|
||||||
onFilterDelete={onTabDelete}
|
) : (
|
||||||
/>
|
displayTabAction === "delete" && (
|
||||||
);
|
<Button
|
||||||
}}
|
className={classes.tabActionButton}
|
||||||
</Debounce>
|
color="primary"
|
||||||
|
onClick={onTabDelete}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Delete Search"
|
||||||
|
description="button"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -44,7 +44,7 @@ const Link: React.FC<LinkProps> = props => {
|
||||||
return (
|
return (
|
||||||
<Typography
|
<Typography
|
||||||
component="a"
|
component="a"
|
||||||
className={classNames({
|
className={classNames(className, {
|
||||||
[classes.root]: true,
|
[classes.root]: true,
|
||||||
[classes[color]]: true,
|
[classes[color]]: true,
|
||||||
[classes.underline]: underline
|
[classes.underline]: underline
|
||||||
|
|
|
@ -26,18 +26,16 @@ const props: MultiAutocompleteSelectFieldProps = {
|
||||||
value: undefined
|
value: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
const Story: React.FC<
|
const Story: React.FC<Partial<
|
||||||
Partial<
|
MultiAutocompleteSelectFieldProps & {
|
||||||
MultiAutocompleteSelectFieldProps & {
|
enableLoadMore: boolean;
|
||||||
enableLoadMore: boolean;
|
}
|
||||||
}
|
>> = ({ allowCustomValues, enableLoadMore }) => {
|
||||||
>
|
|
||||||
> = ({ allowCustomValues, enableLoadMore }) => {
|
|
||||||
const { change, data: countries } = useMultiAutocomplete([suggestions[0]]);
|
const { change, data: countries } = useMultiAutocomplete([suggestions[0]]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChoiceProvider choices={suggestions}>
|
<ChoiceProvider choices={suggestions}>
|
||||||
{({ choices, fetchChoices, fetchMore, hasMore, loading }) => (
|
{({ choices, fetchChoices, onFetchMore, hasMore, loading }) => (
|
||||||
<MultiAutocompleteSelectField
|
<MultiAutocompleteSelectField
|
||||||
{...props}
|
{...props}
|
||||||
displayValues={countries}
|
displayValues={countries}
|
||||||
|
@ -50,7 +48,7 @@ const Story: React.FC<
|
||||||
value={countries.map(country => country.value)}
|
value={countries.map(country => country.value)}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
hasMore={enableLoadMore ? hasMore : false}
|
hasMore={enableLoadMore ? hasMore : false}
|
||||||
onFetchMore={enableLoadMore ? fetchMore : undefined}
|
onFetchMore={enableLoadMore ? onFetchMore : undefined}
|
||||||
allowCustomValues={allowCustomValues}
|
allowCustomValues={allowCustomValues}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -41,7 +41,7 @@ const useStyles = makeStyles(
|
||||||
background: theme.palette.background.default,
|
background: theme.palette.background.default,
|
||||||
borderTop: "1px solid transparent",
|
borderTop: "1px solid transparent",
|
||||||
boxShadow: `0 -5px 5px 0 ${theme.palette.divider}`,
|
boxShadow: `0 -5px 5px 0 ${theme.palette.divider}`,
|
||||||
height: "100%",
|
height: 70,
|
||||||
transition: `box-shadow ${theme.transitions.duration.shortest}ms`
|
transition: `box-shadow ${theme.transitions.duration.shortest}ms`
|
||||||
},
|
},
|
||||||
spacer: {
|
spacer: {
|
||||||
|
|
|
@ -1,15 +1,36 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||||
|
import Button from "@material-ui/core/Button";
|
||||||
|
|
||||||
import { SearchPageProps, TabPageProps } from "@saleor/types";
|
import { SearchPageProps, TabPageProps } from "@saleor/types";
|
||||||
import FilterSearch from "../Filter/FilterSearch";
|
|
||||||
import FilterTabs, { FilterTab } from "../TableFilter";
|
import FilterTabs, { FilterTab } from "../TableFilter";
|
||||||
|
import SearchInput from "./SearchInput";
|
||||||
|
|
||||||
export interface SearchBarProps extends SearchPageProps, TabPageProps {
|
export interface SearchBarProps extends SearchPageProps, TabPageProps {
|
||||||
allTabLabel: string;
|
allTabLabel: string;
|
||||||
searchPlaceholder: string;
|
searchPlaceholder: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
root: {
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
padding: theme.spacing(1, 3)
|
||||||
|
},
|
||||||
|
tabActionButton: {
|
||||||
|
marginLeft: theme.spacing(2),
|
||||||
|
paddingLeft: theme.spacing(3),
|
||||||
|
paddingRight: theme.spacing(3)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "SearchBar"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const SearchBar: React.FC<SearchBarProps> = props => {
|
const SearchBar: React.FC<SearchBarProps> = props => {
|
||||||
const {
|
const {
|
||||||
allTabLabel,
|
allTabLabel,
|
||||||
|
@ -23,9 +44,16 @@ const SearchBar: React.FC<SearchBarProps> = props => {
|
||||||
onTabDelete,
|
onTabDelete,
|
||||||
onTabSave
|
onTabSave
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const classes = useStyles(props);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const isCustom = currentTab === tabs.length + 1;
|
const isCustom = currentTab === tabs.length + 1;
|
||||||
|
const displayTabAction = isCustom
|
||||||
|
? "save"
|
||||||
|
: currentTab === 0
|
||||||
|
? null
|
||||||
|
: "delete";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -47,16 +75,39 @@ const SearchBar: React.FC<SearchBarProps> = props => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</FilterTabs>
|
</FilterTabs>
|
||||||
<FilterSearch
|
<div className={classes.root}>
|
||||||
displaySearchAction={
|
<SearchInput
|
||||||
!!initialSearch ? (isCustom ? "save" : "delete") : null
|
initialSearch={initialSearch}
|
||||||
}
|
placeholder={searchPlaceholder}
|
||||||
initialSearch={initialSearch}
|
onSearchChange={onSearchChange}
|
||||||
searchPlaceholder={searchPlaceholder}
|
/>
|
||||||
onSearchChange={onSearchChange}
|
{displayTabAction &&
|
||||||
onSearchDelete={onTabDelete}
|
(displayTabAction === "save" ? (
|
||||||
onSearchSave={onTabSave}
|
<Button
|
||||||
/>
|
className={classes.tabActionButton}
|
||||||
|
color="primary"
|
||||||
|
onClick={onTabSave}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Save Search"
|
||||||
|
description="button"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
displayTabAction === "delete" && (
|
||||||
|
<Button
|
||||||
|
className={classes.tabActionButton}
|
||||||
|
color="primary"
|
||||||
|
onClick={onTabDelete}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Delete Search"
|
||||||
|
description="button"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
59
src/components/SearchBar/SearchInput.tsx
Normal file
59
src/components/SearchBar/SearchInput.tsx
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import React from "react";
|
||||||
|
import TextField from "@material-ui/core/TextField";
|
||||||
|
|
||||||
|
import { SearchPageProps } from "../../types";
|
||||||
|
import Debounce from "../Debounce";
|
||||||
|
|
||||||
|
export interface SearchInputProps extends SearchPageProps {
|
||||||
|
placeholder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
padding: "10.5px 12px"
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
flex: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SearchInput"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const SearchInput: React.FC<SearchInputProps> = props => {
|
||||||
|
const { initialSearch, onSearchChange, placeholder } = props;
|
||||||
|
|
||||||
|
const classes = useStyles(props);
|
||||||
|
const [search, setSearch] = React.useState(initialSearch);
|
||||||
|
React.useEffect(() => setSearch(initialSearch), [initialSearch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Debounce debounceFn={onSearchChange}>
|
||||||
|
{debounceSearchChange => {
|
||||||
|
const handleSearchChange = (event: React.ChangeEvent<any>) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setSearch(value);
|
||||||
|
debounceSearchChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
className={classes.root}
|
||||||
|
inputProps={{
|
||||||
|
className: classes.input,
|
||||||
|
placeholder
|
||||||
|
}}
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Debounce>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SearchInput.displayName = "SearchInput";
|
||||||
|
export default SearchInput;
|
|
@ -27,20 +27,18 @@ const props: SingleAutocompleteSelectFieldProps = {
|
||||||
value: suggestions[0].value
|
value: suggestions[0].value
|
||||||
};
|
};
|
||||||
|
|
||||||
const Story: React.FC<
|
const Story: React.FC<Partial<
|
||||||
Partial<
|
SingleAutocompleteSelectFieldProps & {
|
||||||
SingleAutocompleteSelectFieldProps & {
|
enableLoadMore: boolean;
|
||||||
enableLoadMore: boolean;
|
}
|
||||||
}
|
>> = ({ allowCustomValues, emptyOption, enableLoadMore }) => {
|
||||||
>
|
|
||||||
> = ({ allowCustomValues, emptyOption, enableLoadMore }) => {
|
|
||||||
const [displayValue, setDisplayValue] = React.useState(suggestions[0].label);
|
const [displayValue, setDisplayValue] = React.useState(suggestions[0].label);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form initial={{ country: suggestions[0].value }}>
|
<Form initial={{ country: suggestions[0].value }}>
|
||||||
{({ change, data }) => (
|
{({ change, data }) => (
|
||||||
<ChoiceProvider choices={suggestions}>
|
<ChoiceProvider choices={suggestions}>
|
||||||
{({ choices, fetchChoices, fetchMore, hasMore, loading }) => {
|
{({ choices, fetchChoices, onFetchMore, hasMore, loading }) => {
|
||||||
const handleSelect = createSingleAutocompleteSelectHandler(
|
const handleSelect = createSingleAutocompleteSelectHandler(
|
||||||
change,
|
change,
|
||||||
setDisplayValue,
|
setDisplayValue,
|
||||||
|
@ -58,7 +56,7 @@ const Story: React.FC<
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
value={data.country}
|
value={data.country}
|
||||||
hasMore={enableLoadMore ? hasMore : false}
|
hasMore={enableLoadMore ? hasMore : false}
|
||||||
onFetchMore={enableLoadMore ? fetchMore : undefined}
|
onFetchMore={enableLoadMore ? onFetchMore : undefined}
|
||||||
allowCustomValues={allowCustomValues}
|
allowCustomValues={allowCustomValues}
|
||||||
emptyOption={emptyOption}
|
emptyOption={emptyOption}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -41,17 +41,14 @@ export interface SingleAutocompleteSelectFieldProps
|
||||||
onChange: (event: React.ChangeEvent<any>) => void;
|
onChange: (event: React.ChangeEvent<any>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DebounceAutocomplete: React.ComponentType<
|
const DebounceAutocomplete: React.ComponentType<DebounceProps<
|
||||||
DebounceProps<string>
|
string
|
||||||
> = Debounce;
|
>> = Debounce;
|
||||||
|
|
||||||
const SingleAutocompleteSelectFieldComponent: React.FC<
|
const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectFieldProps> = props => {
|
||||||
SingleAutocompleteSelectFieldProps
|
|
||||||
> = props => {
|
|
||||||
const {
|
const {
|
||||||
choices,
|
|
||||||
|
|
||||||
allowCustomValues,
|
allowCustomValues,
|
||||||
|
choices,
|
||||||
disabled,
|
disabled,
|
||||||
displayValue,
|
displayValue,
|
||||||
emptyOption,
|
emptyOption,
|
||||||
|
@ -169,9 +166,11 @@ const SingleAutocompleteSelectFieldComponent: React.FC<
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SingleAutocompleteSelectField: React.FC<
|
const SingleAutocompleteSelectField: React.FC<SingleAutocompleteSelectFieldProps> = ({
|
||||||
SingleAutocompleteSelectFieldProps
|
choices,
|
||||||
> = ({ choices, fetchChoices, ...rest }) => {
|
fetchChoices,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
const [query, setQuery] = React.useState("");
|
const [query, setQuery] = React.useState("");
|
||||||
if (fetchChoices) {
|
if (fetchChoices) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { makeStyles } from "@material-ui/core/styles";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { InputProps } from "@material-ui/core/Input";
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
theme => ({
|
theme => ({
|
||||||
|
@ -38,13 +39,13 @@ interface SingleSelectFieldProps {
|
||||||
selectProps?: SelectProps;
|
selectProps?: SelectProps;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
InputProps?: InputProps;
|
||||||
onChange(event: any);
|
onChange(event: any);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SingleSelectField: React.FC<SingleSelectFieldProps> = props => {
|
export const SingleSelectField: React.FC<SingleSelectFieldProps> = props => {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
|
|
||||||
disabled,
|
disabled,
|
||||||
error,
|
error,
|
||||||
label,
|
label,
|
||||||
|
@ -54,7 +55,8 @@ export const SingleSelectField: React.FC<SingleSelectFieldProps> = props => {
|
||||||
name,
|
name,
|
||||||
hint,
|
hint,
|
||||||
selectProps,
|
selectProps,
|
||||||
placeholder
|
placeholder,
|
||||||
|
InputProps
|
||||||
} = props;
|
} = props;
|
||||||
const classes = useStyles(props);
|
const classes = useStyles(props);
|
||||||
|
|
||||||
|
@ -90,6 +92,7 @@ export const SingleSelectField: React.FC<SingleSelectFieldProps> = props => {
|
||||||
}}
|
}}
|
||||||
name={name}
|
name={name}
|
||||||
labelWidth={180}
|
labelWidth={180}
|
||||||
|
{...InputProps}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{...selectProps}
|
{...selectProps}
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
import ButtonBase from "@material-ui/core/ButtonBase";
|
|
||||||
import { makeStyles, useTheme } from "@material-ui/core/styles";
|
|
||||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
|
||||||
import Typography from "@material-ui/core/Typography";
|
|
||||||
import ClearIcon from "@material-ui/icons/Clear";
|
|
||||||
import React from "react";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
|
|
||||||
import FilterActions, { FilterActionsProps } from "../Filter/FilterActions";
|
|
||||||
import Hr from "../Hr";
|
|
||||||
import Link from "../Link";
|
|
||||||
|
|
||||||
export interface Filter {
|
|
||||||
label: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
|
||||||
theme => ({
|
|
||||||
filterButton: {
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: fade(theme.palette.primary.main, 0.8),
|
|
||||||
borderRadius: "19px",
|
|
||||||
display: "flex",
|
|
||||||
height: "38px",
|
|
||||||
justifyContent: "space-around",
|
|
||||||
margin: theme.spacing(0, 1, 2),
|
|
||||||
marginLeft: 0,
|
|
||||||
padding: theme.spacing(0, 2)
|
|
||||||
},
|
|
||||||
filterChipContainer: {
|
|
||||||
display: "flex",
|
|
||||||
flex: 1,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
paddingTop: theme.spacing(2)
|
|
||||||
},
|
|
||||||
filterContainer: {
|
|
||||||
"& a": {
|
|
||||||
paddingBottom: 10,
|
|
||||||
paddingTop: theme.spacing(1)
|
|
||||||
},
|
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
||||||
display: "flex",
|
|
||||||
marginTop: -theme.spacing(1),
|
|
||||||
padding: theme.spacing(0, 2)
|
|
||||||
},
|
|
||||||
filterIcon: {
|
|
||||||
color: theme.palette.common.white,
|
|
||||||
height: 16,
|
|
||||||
width: 16
|
|
||||||
},
|
|
||||||
filterIconContainer: {
|
|
||||||
WebkitAppearance: "none",
|
|
||||||
background: "transparent",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "100%",
|
|
||||||
cursor: "pointer",
|
|
||||||
height: 32,
|
|
||||||
marginRight: -13,
|
|
||||||
padding: 8,
|
|
||||||
width: 32
|
|
||||||
},
|
|
||||||
filterLabel: {
|
|
||||||
marginBottom: theme.spacing(1)
|
|
||||||
},
|
|
||||||
filterText: {
|
|
||||||
color: theme.palette.common.white,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 400 as 400,
|
|
||||||
lineHeight: "38px"
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: "FilterChips"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
interface FilterChipProps extends FilterActionsProps {
|
|
||||||
displayTabAction: "save" | "delete" | null;
|
|
||||||
filtersList: Filter[];
|
|
||||||
search: string;
|
|
||||||
isCustomSearch: boolean;
|
|
||||||
onFilterDelete: () => void;
|
|
||||||
onFilterSave: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FilterChips: React.FC<FilterChipProps> = ({
|
|
||||||
currencySymbol,
|
|
||||||
displayTabAction,
|
|
||||||
filtersList,
|
|
||||||
menu,
|
|
||||||
filterLabel,
|
|
||||||
placeholder,
|
|
||||||
onSearchChange,
|
|
||||||
search,
|
|
||||||
onFilterAdd,
|
|
||||||
onFilterSave,
|
|
||||||
onFilterDelete
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const classes = useStyles({ theme });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FilterActions
|
|
||||||
currencySymbol={currencySymbol}
|
|
||||||
menu={menu}
|
|
||||||
filterLabel={filterLabel}
|
|
||||||
placeholder={placeholder}
|
|
||||||
search={search}
|
|
||||||
onSearchChange={onSearchChange}
|
|
||||||
onFilterAdd={onFilterAdd}
|
|
||||||
/>
|
|
||||||
{search || (filtersList && filtersList.length > 0) ? (
|
|
||||||
<div className={classes.filterContainer}>
|
|
||||||
<div className={classes.filterChipContainer}>
|
|
||||||
{filtersList.map(filter => (
|
|
||||||
<div className={classes.filterButton} key={filter.label}>
|
|
||||||
<Typography className={classes.filterText}>
|
|
||||||
{filter.label}
|
|
||||||
</Typography>
|
|
||||||
<ButtonBase
|
|
||||||
className={classes.filterIconContainer}
|
|
||||||
onClick={filter.onClick}
|
|
||||||
>
|
|
||||||
<ClearIcon className={classes.filterIcon} />
|
|
||||||
</ButtonBase>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{displayTabAction === "save" ? (
|
|
||||||
<Link onClick={onFilterSave}>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Save Custom Search"
|
|
||||||
description="button"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
displayTabAction === "delete" && (
|
|
||||||
<Link onClick={onFilterDelete}>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Delete Search"
|
|
||||||
description="button"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Hr />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FilterChips;
|
|
|
@ -1,4 +1,3 @@
|
||||||
export { default } from "./FilterTabs";
|
export { default } from "./FilterTabs";
|
||||||
export * from "./FilterTabs";
|
export * from "./FilterTabs";
|
||||||
export * from "./FilterTab";
|
export * from "./FilterTab";
|
||||||
export * from "./FilterChips";
|
|
||||||
|
|
|
@ -5,33 +5,41 @@ import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import Container from "@saleor/components/Container";
|
import Container from "@saleor/components/Container";
|
||||||
import PageHeader from "@saleor/components/PageHeader";
|
import PageHeader from "@saleor/components/PageHeader";
|
||||||
import SearchBar from "@saleor/components/SearchBar";
|
|
||||||
import { sectionNames } from "@saleor/intl";
|
import { sectionNames } from "@saleor/intl";
|
||||||
import {
|
import {
|
||||||
ListActions,
|
ListActions,
|
||||||
PageListProps,
|
PageListProps,
|
||||||
SearchPageProps,
|
|
||||||
TabPageProps,
|
TabPageProps,
|
||||||
SortPage
|
SortPage,
|
||||||
|
FilterPageProps
|
||||||
} from "@saleor/types";
|
} from "@saleor/types";
|
||||||
import { CustomerListUrlSortField } from "@saleor/customers/urls";
|
import { CustomerListUrlSortField } from "@saleor/customers/urls";
|
||||||
import { ListCustomers_customers_edges_node } from "../../types/ListCustomers";
|
import FilterBar from "@saleor/components/FilterBar";
|
||||||
import CustomerList from "../CustomerList/CustomerList";
|
import CustomerList from "../CustomerList/CustomerList";
|
||||||
|
import { ListCustomers_customers_edges_node } from "../../types/ListCustomers";
|
||||||
|
import {
|
||||||
|
CustomerFilterKeys,
|
||||||
|
CustomerListFilterOpts,
|
||||||
|
createFilterStructure
|
||||||
|
} from "./filters";
|
||||||
|
|
||||||
export interface CustomerListPageProps
|
export interface CustomerListPageProps
|
||||||
extends PageListProps,
|
extends PageListProps,
|
||||||
ListActions,
|
ListActions,
|
||||||
SearchPageProps,
|
FilterPageProps<CustomerFilterKeys, CustomerListFilterOpts>,
|
||||||
SortPage<CustomerListUrlSortField>,
|
SortPage<CustomerListUrlSortField>,
|
||||||
TabPageProps {
|
TabPageProps {
|
||||||
customers: ListCustomers_customers_edges_node[];
|
customers: ListCustomers_customers_edges_node[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomerListPage: React.FC<CustomerListPageProps> = ({
|
const CustomerListPage: React.FC<CustomerListPageProps> = ({
|
||||||
|
currencySymbol,
|
||||||
currentTab,
|
currentTab,
|
||||||
|
filterOpts,
|
||||||
initialSearch,
|
initialSearch,
|
||||||
onAdd,
|
onAdd,
|
||||||
onAll,
|
onAll,
|
||||||
|
onFilterChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
onTabDelete,
|
onTabDelete,
|
||||||
|
@ -41,6 +49,8 @@ const CustomerListPage: React.FC<CustomerListPageProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const structure = createFilterStructure(intl, filterOpts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<PageHeader title={intl.formatMessage(sectionNames.customers)}>
|
<PageHeader title={intl.formatMessage(sectionNames.customers)}>
|
||||||
|
@ -52,18 +62,21 @@ const CustomerListPage: React.FC<CustomerListPageProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Card>
|
<Card>
|
||||||
<SearchBar
|
<FilterBar
|
||||||
allTabLabel={intl.formatMessage({
|
allTabLabel={intl.formatMessage({
|
||||||
defaultMessage: "All Customers",
|
defaultMessage: "All Customers",
|
||||||
description: "tab name"
|
description: "tab name"
|
||||||
})}
|
})}
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
filterStructure={structure}
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
searchPlaceholder={intl.formatMessage({
|
searchPlaceholder={intl.formatMessage({
|
||||||
defaultMessage: "Search Customer"
|
defaultMessage: "Search Customer"
|
||||||
})}
|
})}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
onAll={onAll}
|
onAll={onAll}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
onTabChange={onTabChange}
|
onTabChange={onTabChange}
|
||||||
onTabDelete={onTabDelete}
|
onTabDelete={onTabDelete}
|
||||||
|
|
66
src/customers/components/CustomerListPage/filters.ts
Normal file
66
src/customers/components/CustomerListPage/filters.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { IntlShape, defineMessages } from "react-intl";
|
||||||
|
|
||||||
|
import { FilterOpts, MinMax } from "@saleor/types";
|
||||||
|
import { IFilter } from "@saleor/components/Filter";
|
||||||
|
import {
|
||||||
|
createDateField,
|
||||||
|
createNumberField
|
||||||
|
} from "@saleor/utils/filters/fields";
|
||||||
|
|
||||||
|
export enum CustomerFilterKeys {
|
||||||
|
joined = "joined",
|
||||||
|
moneySpent = "spent",
|
||||||
|
numberOfOrders = "orders"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerListFilterOpts {
|
||||||
|
joined: FilterOpts<MinMax>;
|
||||||
|
moneySpent: FilterOpts<MinMax>;
|
||||||
|
numberOfOrders: FilterOpts<MinMax>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
joinDate: {
|
||||||
|
defaultMessage: "Join Date",
|
||||||
|
description: "customer"
|
||||||
|
},
|
||||||
|
moneySpent: {
|
||||||
|
defaultMessage: "Money Spent",
|
||||||
|
description: "customer"
|
||||||
|
},
|
||||||
|
numberOfOrders: {
|
||||||
|
defaultMessage: "Number of Orders"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createFilterStructure(
|
||||||
|
intl: IntlShape,
|
||||||
|
opts: CustomerListFilterOpts
|
||||||
|
): IFilter<CustomerFilterKeys> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...createDateField(
|
||||||
|
CustomerFilterKeys.joined,
|
||||||
|
intl.formatMessage(messages.joinDate),
|
||||||
|
opts.joined.value
|
||||||
|
),
|
||||||
|
active: opts.joined.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createNumberField(
|
||||||
|
CustomerFilterKeys.moneySpent,
|
||||||
|
intl.formatMessage(messages.moneySpent),
|
||||||
|
opts.moneySpent.value
|
||||||
|
),
|
||||||
|
active: opts.moneySpent.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createNumberField(
|
||||||
|
CustomerFilterKeys.numberOfOrders,
|
||||||
|
intl.formatMessage(messages.numberOfOrders),
|
||||||
|
opts.numberOfOrders.value
|
||||||
|
),
|
||||||
|
active: opts.numberOfOrders.active
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
export { default } from "./CustomerListPage";
|
export { default } from "./CustomerListPage";
|
||||||
export * from "./CustomerListPage";
|
export * from "./CustomerListPage";
|
||||||
|
export * from "./filters";
|
||||||
|
|
|
@ -16,6 +16,12 @@ export const customerSection = "/customers/";
|
||||||
|
|
||||||
export const customerListPath = customerSection;
|
export const customerListPath = customerSection;
|
||||||
export enum CustomerListUrlFiltersEnum {
|
export enum CustomerListUrlFiltersEnum {
|
||||||
|
joinedFrom = "joinedFrom",
|
||||||
|
joinedTo = "joinedTo",
|
||||||
|
moneySpentFrom = "moneySpentFrom",
|
||||||
|
moneySpentTo = "moneySpentTo",
|
||||||
|
numberOfOrdersFrom = "numberOfOrdersFrom",
|
||||||
|
numberOfOrdersTo = "numberOfOrdersTo",
|
||||||
query = "query"
|
query = "query"
|
||||||
}
|
}
|
||||||
export type CustomerListUrlFilters = Filters<CustomerListUrlFiltersEnum>;
|
export type CustomerListUrlFilters = Filters<CustomerListUrlFiltersEnum>;
|
||||||
|
|
|
@ -22,6 +22,8 @@ import { ListViews } from "@saleor/types";
|
||||||
import { getSortParams } from "@saleor/utils/sort";
|
import { getSortParams } from "@saleor/utils/sort";
|
||||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||||
|
import useShop from "@saleor/hooks/useShop";
|
||||||
|
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||||
import CustomerListPage from "../../components/CustomerListPage";
|
import CustomerListPage from "../../components/CustomerListPage";
|
||||||
import { TypedBulkRemoveCustomers } from "../../mutations";
|
import { TypedBulkRemoveCustomers } from "../../mutations";
|
||||||
import { useCustomerListQuery } from "../../queries";
|
import { useCustomerListQuery } from "../../queries";
|
||||||
|
@ -29,7 +31,6 @@ import { BulkRemoveCustomers } from "../../types/BulkRemoveCustomers";
|
||||||
import {
|
import {
|
||||||
customerAddUrl,
|
customerAddUrl,
|
||||||
customerListUrl,
|
customerListUrl,
|
||||||
CustomerListUrlFilters,
|
|
||||||
CustomerListUrlQueryParams,
|
CustomerListUrlQueryParams,
|
||||||
customerUrl,
|
customerUrl,
|
||||||
CustomerListUrlDialog
|
CustomerListUrlDialog
|
||||||
|
@ -40,8 +41,10 @@ import {
|
||||||
getActiveFilters,
|
getActiveFilters,
|
||||||
getFilterTabs,
|
getFilterTabs,
|
||||||
getFilterVariables,
|
getFilterVariables,
|
||||||
saveFilterTab
|
saveFilterTab,
|
||||||
} from "./filter";
|
getFilterQueryParam,
|
||||||
|
getFilterOpts
|
||||||
|
} from "./filters";
|
||||||
import { getSortQueryVariables } from "./sort";
|
import { getSortQueryVariables } from "./sort";
|
||||||
|
|
||||||
interface CustomerListProps {
|
interface CustomerListProps {
|
||||||
|
@ -59,6 +62,7 @@ export const CustomerList: React.FC<CustomerListProps> = ({ params }) => {
|
||||||
ListViews.CUSTOMER_LIST
|
ListViews.CUSTOMER_LIST
|
||||||
);
|
);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const shop = useShop();
|
||||||
|
|
||||||
const paginationState = createPaginationState(settings.rowNumber, params);
|
const paginationState = createPaginationState(settings.rowNumber, params);
|
||||||
const queryVariables = React.useMemo(
|
const queryVariables = React.useMemo(
|
||||||
|
@ -83,16 +87,17 @@ export const CustomerList: React.FC<CustomerListProps> = ({ params }) => {
|
||||||
: 0
|
: 0
|
||||||
: parseInt(params.activeTab, 0);
|
: parseInt(params.activeTab, 0);
|
||||||
|
|
||||||
const changeFilterField = (filter: CustomerListUrlFilters) => {
|
const [
|
||||||
reset();
|
changeFilters,
|
||||||
navigate(
|
resetFilters,
|
||||||
customerListUrl({
|
handleSearchChange
|
||||||
...getActiveFilters(params),
|
] = createFilterHandlers({
|
||||||
...filter,
|
cleanupFn: reset,
|
||||||
activeTab: undefined
|
createUrl: customerListUrl,
|
||||||
})
|
getFilterQueryParam,
|
||||||
);
|
navigate,
|
||||||
};
|
params
|
||||||
|
});
|
||||||
|
|
||||||
const [openModal, closeModal] = createDialogActionHandlers<
|
const [openModal, closeModal] = createDialogActionHandlers<
|
||||||
CustomerListUrlDialog,
|
CustomerListUrlDialog,
|
||||||
|
@ -138,16 +143,20 @@ export const CustomerList: React.FC<CustomerListProps> = ({ params }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSort = createSortHandler(navigate, customerListUrl, params);
|
const handleSort = createSortHandler(navigate, customerListUrl, params);
|
||||||
|
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TypedBulkRemoveCustomers onCompleted={handleBulkCustomerDelete}>
|
<TypedBulkRemoveCustomers onCompleted={handleBulkCustomerDelete}>
|
||||||
{(bulkRemoveCustomers, bulkRemoveCustomersOpts) => (
|
{(bulkRemoveCustomers, bulkRemoveCustomersOpts) => (
|
||||||
<>
|
<>
|
||||||
<CustomerListPage
|
<CustomerListPage
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
filterOpts={getFilterOpts(params)}
|
||||||
initialSearch={params.query || ""}
|
initialSearch={params.query || ""}
|
||||||
onSearchChange={query => changeFilterField({ query })}
|
onSearchChange={handleSearchChange}
|
||||||
onAll={() => navigate(customerListUrl())}
|
onFilterChange={changeFilters}
|
||||||
|
onAll={resetFilters}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
onTabDelete={() => openModal("delete-search")}
|
onTabDelete={() => openModal("delete-search")}
|
||||||
onTabSave={() => openModal("save-search")}
|
onTabSave={() => openModal("save-search")}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
|
||||||
|
Object {
|
||||||
|
"joinedFrom": "2019-12-09",
|
||||||
|
"joinedTo": "2019-12-38",
|
||||||
|
"moneySpentFrom": "2",
|
||||||
|
"moneySpentTo": "39.50",
|
||||||
|
"numberOfOrdersFrom": "1",
|
||||||
|
"numberOfOrdersTo": "5",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"joinedFrom=2019-12-09&joinedTo=2019-12-38&moneySpentFrom=2&moneySpentTo=39.50&numberOfOrdersFrom=1&numberOfOrdersTo=5"`;
|
|
@ -1,31 +0,0 @@
|
||||||
import { CustomerFilterInput } from "@saleor/types/globalTypes";
|
|
||||||
import {
|
|
||||||
createFilterTabUtils,
|
|
||||||
createFilterUtils
|
|
||||||
} from "../../../utils/filters";
|
|
||||||
import {
|
|
||||||
CustomerListUrlFilters,
|
|
||||||
CustomerListUrlFiltersEnum,
|
|
||||||
CustomerListUrlQueryParams
|
|
||||||
} from "../../urls";
|
|
||||||
|
|
||||||
export const CUSTOMER_FILTERS_KEY = "customerFilters";
|
|
||||||
|
|
||||||
export function getFilterVariables(
|
|
||||||
params: CustomerListUrlFilters
|
|
||||||
): CustomerFilterInput {
|
|
||||||
return {
|
|
||||||
search: params.query
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const {
|
|
||||||
deleteFilterTab,
|
|
||||||
getFilterTabs,
|
|
||||||
saveFilterTab
|
|
||||||
} = createFilterTabUtils<CustomerListUrlFilters>(CUSTOMER_FILTERS_KEY);
|
|
||||||
|
|
||||||
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
|
|
||||||
CustomerListUrlQueryParams,
|
|
||||||
CustomerListUrlFilters
|
|
||||||
>(CustomerListUrlFiltersEnum);
|
|
78
src/customers/views/CustomerList/filters.test.ts
Normal file
78
src/customers/views/CustomerList/filters.test.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import { createIntl } from "react-intl";
|
||||||
|
import { stringify as stringifyQs } from "qs";
|
||||||
|
|
||||||
|
import { CustomerListUrlFilters } from "@saleor/customers/urls";
|
||||||
|
import { createFilterStructure } from "@saleor/customers/components/CustomerListPage";
|
||||||
|
import { getFilterQueryParams } from "@saleor/utils/filters";
|
||||||
|
import { date } from "@saleor/fixtures";
|
||||||
|
import { getExistingKeys, setFilterOptsStatus } from "@test/filters";
|
||||||
|
import { config } from "@test/intl";
|
||||||
|
import { getFilterVariables, getFilterQueryParam } from "./filters";
|
||||||
|
|
||||||
|
describe("Filtering query params", () => {
|
||||||
|
it("should be empty object if no params given", () => {
|
||||||
|
const params: CustomerListUrlFilters = {};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterVariables)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be empty object if params given", () => {
|
||||||
|
const params: CustomerListUrlFilters = {
|
||||||
|
joinedFrom: date.from,
|
||||||
|
moneySpentFrom: "2",
|
||||||
|
moneySpentTo: "39.50",
|
||||||
|
numberOfOrdersTo: "5"
|
||||||
|
};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterVariables)).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Filtering URL params", () => {
|
||||||
|
const intl = createIntl(config);
|
||||||
|
|
||||||
|
const filters = createFilterStructure(intl, {
|
||||||
|
joined: {
|
||||||
|
active: false,
|
||||||
|
value: {
|
||||||
|
max: date.to,
|
||||||
|
min: date.from
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moneySpent: {
|
||||||
|
active: false,
|
||||||
|
value: {
|
||||||
|
max: "39.50",
|
||||||
|
min: "2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
numberOfOrders: {
|
||||||
|
active: false,
|
||||||
|
value: {
|
||||||
|
max: "5",
|
||||||
|
min: "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be empty if no active filters", () => {
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
filters,
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterQueryParams)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be empty if active filters are present", () => {
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
setFilterOptsStatus(filters, true),
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(filterQueryParams).toMatchSnapshot();
|
||||||
|
expect(stringifyQs(filterQueryParams)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
126
src/customers/views/CustomerList/filters.ts
Normal file
126
src/customers/views/CustomerList/filters.ts
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import { CustomerFilterInput } from "@saleor/types/globalTypes";
|
||||||
|
import { maybe } from "@saleor/misc";
|
||||||
|
import { IFilterElement } from "@saleor/components/Filter";
|
||||||
|
import {
|
||||||
|
CustomerFilterKeys,
|
||||||
|
CustomerListFilterOpts
|
||||||
|
} from "@saleor/customers/components/CustomerListPage";
|
||||||
|
import {
|
||||||
|
createFilterTabUtils,
|
||||||
|
createFilterUtils,
|
||||||
|
getGteLteVariables,
|
||||||
|
getMinMaxQueryParam
|
||||||
|
} from "../../../utils/filters";
|
||||||
|
import {
|
||||||
|
CustomerListUrlFilters,
|
||||||
|
CustomerListUrlFiltersEnum,
|
||||||
|
CustomerListUrlQueryParams
|
||||||
|
} from "../../urls";
|
||||||
|
|
||||||
|
export const CUSTOMER_FILTERS_KEY = "customerFilters";
|
||||||
|
|
||||||
|
export function getFilterOpts(
|
||||||
|
params: CustomerListUrlFilters
|
||||||
|
): CustomerListFilterOpts {
|
||||||
|
return {
|
||||||
|
joined: {
|
||||||
|
active: maybe(
|
||||||
|
() =>
|
||||||
|
[params.joinedFrom, params.joinedTo].some(
|
||||||
|
field => field !== undefined
|
||||||
|
),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
max: maybe(() => params.joinedTo, ""),
|
||||||
|
min: maybe(() => params.joinedFrom, "")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moneySpent: {
|
||||||
|
active: maybe(
|
||||||
|
() =>
|
||||||
|
[params.moneySpentFrom, params.moneySpentTo].some(
|
||||||
|
field => field !== undefined
|
||||||
|
),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
max: maybe(() => params.moneySpentTo, ""),
|
||||||
|
min: maybe(() => params.moneySpentFrom, "")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
numberOfOrders: {
|
||||||
|
active: maybe(
|
||||||
|
() =>
|
||||||
|
[params.numberOfOrdersFrom, params.numberOfOrdersTo].some(
|
||||||
|
field => field !== undefined
|
||||||
|
),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
max: maybe(() => params.numberOfOrdersTo, ""),
|
||||||
|
min: maybe(() => params.numberOfOrdersFrom, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilterVariables(
|
||||||
|
params: CustomerListUrlFilters
|
||||||
|
): CustomerFilterInput {
|
||||||
|
return {
|
||||||
|
dateJoined: getGteLteVariables({
|
||||||
|
gte: params.joinedFrom,
|
||||||
|
lte: params.joinedTo
|
||||||
|
}),
|
||||||
|
moneySpent: getGteLteVariables({
|
||||||
|
gte: parseInt(params.moneySpentFrom, 0),
|
||||||
|
lte: parseInt(params.moneySpentTo, 0)
|
||||||
|
}),
|
||||||
|
numberOfOrders: getGteLteVariables({
|
||||||
|
gte: parseInt(params.numberOfOrdersFrom, 0),
|
||||||
|
lte: parseInt(params.numberOfOrdersTo, 0)
|
||||||
|
}),
|
||||||
|
search: params.query
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilterQueryParam(
|
||||||
|
filter: IFilterElement<CustomerFilterKeys>
|
||||||
|
): CustomerListUrlFilters {
|
||||||
|
const { name } = filter;
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case CustomerFilterKeys.joined:
|
||||||
|
return getMinMaxQueryParam(
|
||||||
|
filter,
|
||||||
|
CustomerListUrlFiltersEnum.joinedFrom,
|
||||||
|
CustomerListUrlFiltersEnum.joinedTo
|
||||||
|
);
|
||||||
|
|
||||||
|
case CustomerFilterKeys.moneySpent:
|
||||||
|
return getMinMaxQueryParam(
|
||||||
|
filter,
|
||||||
|
CustomerListUrlFiltersEnum.moneySpentFrom,
|
||||||
|
CustomerListUrlFiltersEnum.moneySpentTo
|
||||||
|
);
|
||||||
|
|
||||||
|
case CustomerFilterKeys.numberOfOrders:
|
||||||
|
return getMinMaxQueryParam(
|
||||||
|
filter,
|
||||||
|
CustomerListUrlFiltersEnum.numberOfOrdersFrom,
|
||||||
|
CustomerListUrlFiltersEnum.numberOfOrdersTo
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const {
|
||||||
|
deleteFilterTab,
|
||||||
|
getFilterTabs,
|
||||||
|
saveFilterTab
|
||||||
|
} = createFilterTabUtils<CustomerListUrlFilters>(CUSTOMER_FILTERS_KEY);
|
||||||
|
|
||||||
|
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
|
||||||
|
CustomerListUrlQueryParams,
|
||||||
|
CustomerListUrlFilters
|
||||||
|
>(CustomerListUrlFiltersEnum);
|
|
@ -5,23 +5,28 @@ import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import Container from "@saleor/components/Container";
|
import Container from "@saleor/components/Container";
|
||||||
import PageHeader from "@saleor/components/PageHeader";
|
import PageHeader from "@saleor/components/PageHeader";
|
||||||
import SearchBar from "@saleor/components/SearchBar";
|
import FilterBar from "@saleor/components/FilterBar";
|
||||||
import { sectionNames } from "@saleor/intl";
|
import { sectionNames } from "@saleor/intl";
|
||||||
import {
|
import {
|
||||||
ListActions,
|
ListActions,
|
||||||
PageListProps,
|
PageListProps,
|
||||||
SearchPageProps,
|
|
||||||
TabPageProps,
|
TabPageProps,
|
||||||
SortPage
|
SortPage,
|
||||||
|
FilterPageProps
|
||||||
} from "@saleor/types";
|
} from "@saleor/types";
|
||||||
import { SaleListUrlSortField } from "@saleor/discounts/urls";
|
import { SaleListUrlSortField } from "@saleor/discounts/urls";
|
||||||
import { SaleList_sales_edges_node } from "../../types/SaleList";
|
import { SaleList_sales_edges_node } from "../../types/SaleList";
|
||||||
import SaleList from "../SaleList";
|
import SaleList from "../SaleList";
|
||||||
|
import {
|
||||||
|
SaleFilterKeys,
|
||||||
|
SaleListFilterOpts,
|
||||||
|
createFilterStructure
|
||||||
|
} from "./filters";
|
||||||
|
|
||||||
export interface SaleListPageProps
|
export interface SaleListPageProps
|
||||||
extends PageListProps,
|
extends PageListProps,
|
||||||
ListActions,
|
ListActions,
|
||||||
SearchPageProps,
|
FilterPageProps<SaleFilterKeys, SaleListFilterOpts>,
|
||||||
SortPage<SaleListUrlSortField>,
|
SortPage<SaleListUrlSortField>,
|
||||||
TabPageProps {
|
TabPageProps {
|
||||||
defaultCurrency: string;
|
defaultCurrency: string;
|
||||||
|
@ -29,10 +34,13 @@ export interface SaleListPageProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const SaleListPage: React.FC<SaleListPageProps> = ({
|
const SaleListPage: React.FC<SaleListPageProps> = ({
|
||||||
|
currencySymbol,
|
||||||
currentTab,
|
currentTab,
|
||||||
|
filterOpts,
|
||||||
initialSearch,
|
initialSearch,
|
||||||
onAdd,
|
onAdd,
|
||||||
onAll,
|
onAll,
|
||||||
|
onFilterChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
onTabDelete,
|
onTabDelete,
|
||||||
|
@ -42,6 +50,8 @@ const SaleListPage: React.FC<SaleListPageProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const structure = createFilterStructure(intl, filterOpts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<PageHeader title={intl.formatMessage(sectionNames.sales)}>
|
<PageHeader title={intl.formatMessage(sectionNames.sales)}>
|
||||||
|
@ -50,18 +60,21 @@ const SaleListPage: React.FC<SaleListPageProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Card>
|
<Card>
|
||||||
<SearchBar
|
<FilterBar
|
||||||
allTabLabel={intl.formatMessage({
|
allTabLabel={intl.formatMessage({
|
||||||
defaultMessage: "All Sales",
|
defaultMessage: "All Sales",
|
||||||
description: "tab name"
|
description: "tab name"
|
||||||
})}
|
})}
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
filterStructure={structure}
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
searchPlaceholder={intl.formatMessage({
|
searchPlaceholder={intl.formatMessage({
|
||||||
defaultMessage: "Search Sale"
|
defaultMessage: "Search Sale"
|
||||||
})}
|
})}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
onAll={onAll}
|
onAll={onAll}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
onTabChange={onTabChange}
|
onTabChange={onTabChange}
|
||||||
onTabDelete={onTabDelete}
|
onTabDelete={onTabDelete}
|
||||||
|
|
116
src/discounts/components/SaleListPage/filters.ts
Normal file
116
src/discounts/components/SaleListPage/filters.ts
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import { defineMessages, IntlShape } from "react-intl";
|
||||||
|
|
||||||
|
import { FilterOpts, MinMax } from "@saleor/types";
|
||||||
|
import {
|
||||||
|
DiscountStatusEnum,
|
||||||
|
DiscountValueTypeEnum
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
|
import {
|
||||||
|
createDateField,
|
||||||
|
createOptionsField
|
||||||
|
} from "@saleor/utils/filters/fields";
|
||||||
|
import { IFilter } from "@saleor/components/Filter";
|
||||||
|
|
||||||
|
export enum SaleFilterKeys {
|
||||||
|
saleType = "saleType",
|
||||||
|
started = "started",
|
||||||
|
status = "status"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaleListFilterOpts {
|
||||||
|
saleType: FilterOpts<DiscountValueTypeEnum>;
|
||||||
|
started: FilterOpts<MinMax>;
|
||||||
|
status: FilterOpts<DiscountStatusEnum[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
active: {
|
||||||
|
defaultMessage: "Active",
|
||||||
|
description: "sale status"
|
||||||
|
},
|
||||||
|
expired: {
|
||||||
|
defaultMessage: "Expired",
|
||||||
|
description: "sale status"
|
||||||
|
},
|
||||||
|
fixed: {
|
||||||
|
defaultMessage: "Fixed amount",
|
||||||
|
description: "discount type"
|
||||||
|
},
|
||||||
|
percentage: {
|
||||||
|
defaultMessage: "Percentage",
|
||||||
|
description: "discount type"
|
||||||
|
},
|
||||||
|
scheduled: {
|
||||||
|
defaultMessage: "Scheduled",
|
||||||
|
description: "sale status"
|
||||||
|
},
|
||||||
|
started: {
|
||||||
|
defaultMessage: "Started",
|
||||||
|
description: "sale start date"
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
defaultMessage: "Status",
|
||||||
|
description: "sale status"
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
defaultMessage: "Discount Type"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createFilterStructure(
|
||||||
|
intl: IntlShape,
|
||||||
|
opts: SaleListFilterOpts
|
||||||
|
): IFilter<SaleFilterKeys> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...createDateField(
|
||||||
|
SaleFilterKeys.started,
|
||||||
|
intl.formatMessage(messages.started),
|
||||||
|
opts.started.value
|
||||||
|
),
|
||||||
|
active: opts.started.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createOptionsField(
|
||||||
|
SaleFilterKeys.status,
|
||||||
|
intl.formatMessage(messages.status),
|
||||||
|
opts.status.value,
|
||||||
|
true,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.active),
|
||||||
|
value: DiscountStatusEnum.ACTIVE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.expired),
|
||||||
|
value: DiscountStatusEnum.EXPIRED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.scheduled),
|
||||||
|
value: DiscountStatusEnum.SCHEDULED
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
active: opts.status.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createOptionsField(
|
||||||
|
SaleFilterKeys.saleType,
|
||||||
|
intl.formatMessage(messages.type),
|
||||||
|
[opts.saleType.value],
|
||||||
|
false,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.fixed),
|
||||||
|
value: DiscountValueTypeEnum.FIXED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.percentage),
|
||||||
|
value: DiscountValueTypeEnum.PERCENTAGE
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
active: opts.saleType.active
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
export { default } from "./SaleListPage";
|
export { default } from "./SaleListPage";
|
||||||
export * from "./SaleListPage";
|
export * from "./SaleListPage";
|
||||||
|
export * from "./filters";
|
||||||
|
|
|
@ -5,23 +5,28 @@ import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import Container from "@saleor/components/Container";
|
import Container from "@saleor/components/Container";
|
||||||
import PageHeader from "@saleor/components/PageHeader";
|
import PageHeader from "@saleor/components/PageHeader";
|
||||||
import SearchBar from "@saleor/components/SearchBar";
|
import FilterBar from "@saleor/components/FilterBar";
|
||||||
import { sectionNames } from "@saleor/intl";
|
import { sectionNames } from "@saleor/intl";
|
||||||
import {
|
import {
|
||||||
ListActions,
|
ListActions,
|
||||||
PageListProps,
|
PageListProps,
|
||||||
SearchPageProps,
|
|
||||||
TabPageProps,
|
TabPageProps,
|
||||||
SortPage
|
SortPage,
|
||||||
|
FilterPageProps
|
||||||
} from "@saleor/types";
|
} from "@saleor/types";
|
||||||
import { VoucherListUrlSortField } from "@saleor/discounts/urls";
|
import { VoucherListUrlSortField } from "@saleor/discounts/urls";
|
||||||
import { VoucherList_vouchers_edges_node } from "../../types/VoucherList";
|
import { VoucherList_vouchers_edges_node } from "../../types/VoucherList";
|
||||||
import VoucherList from "../VoucherList";
|
import VoucherList from "../VoucherList";
|
||||||
|
import {
|
||||||
|
VoucherFilterKeys,
|
||||||
|
VoucherListFilterOpts,
|
||||||
|
createFilterStructure
|
||||||
|
} from "./filters";
|
||||||
|
|
||||||
export interface VoucherListPageProps
|
export interface VoucherListPageProps
|
||||||
extends PageListProps,
|
extends PageListProps,
|
||||||
ListActions,
|
ListActions,
|
||||||
SearchPageProps,
|
FilterPageProps<VoucherFilterKeys, VoucherListFilterOpts>,
|
||||||
SortPage<VoucherListUrlSortField>,
|
SortPage<VoucherListUrlSortField>,
|
||||||
TabPageProps {
|
TabPageProps {
|
||||||
defaultCurrency: string;
|
defaultCurrency: string;
|
||||||
|
@ -29,10 +34,13 @@ export interface VoucherListPageProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const VoucherListPage: React.FC<VoucherListPageProps> = ({
|
const VoucherListPage: React.FC<VoucherListPageProps> = ({
|
||||||
|
currencySymbol,
|
||||||
currentTab,
|
currentTab,
|
||||||
|
filterOpts,
|
||||||
initialSearch,
|
initialSearch,
|
||||||
onAdd,
|
onAdd,
|
||||||
onAll,
|
onAll,
|
||||||
|
onFilterChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
onTabDelete,
|
onTabDelete,
|
||||||
|
@ -42,6 +50,8 @@ const VoucherListPage: React.FC<VoucherListPageProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const structure = createFilterStructure(intl, filterOpts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<PageHeader title={intl.formatMessage(sectionNames.vouchers)}>
|
<PageHeader title={intl.formatMessage(sectionNames.vouchers)}>
|
||||||
|
@ -53,18 +63,21 @@ const VoucherListPage: React.FC<VoucherListPageProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Card>
|
<Card>
|
||||||
<SearchBar
|
<FilterBar
|
||||||
allTabLabel={intl.formatMessage({
|
allTabLabel={intl.formatMessage({
|
||||||
defaultMessage: "All Vouchers",
|
defaultMessage: "All Vouchers",
|
||||||
description: "tab name"
|
description: "tab name"
|
||||||
})}
|
})}
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
filterStructure={structure}
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
searchPlaceholder={intl.formatMessage({
|
searchPlaceholder={intl.formatMessage({
|
||||||
defaultMessage: "Search Voucher"
|
defaultMessage: "Search Voucher"
|
||||||
})}
|
})}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
onAll={onAll}
|
onAll={onAll}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
onTabChange={onTabChange}
|
onTabChange={onTabChange}
|
||||||
onTabDelete={onTabDelete}
|
onTabDelete={onTabDelete}
|
||||||
|
|
135
src/discounts/components/VoucherListPage/filters.ts
Normal file
135
src/discounts/components/VoucherListPage/filters.ts
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import { defineMessages, IntlShape } from "react-intl";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createOptionsField,
|
||||||
|
createNumberField,
|
||||||
|
createDateField
|
||||||
|
} from "@saleor/utils/filters/fields";
|
||||||
|
import {
|
||||||
|
VoucherDiscountType,
|
||||||
|
DiscountStatusEnum
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
|
import { MinMax, FilterOpts } from "@saleor/types";
|
||||||
|
import { IFilter } from "@saleor/components/Filter";
|
||||||
|
|
||||||
|
export enum VoucherFilterKeys {
|
||||||
|
saleType = "saleType",
|
||||||
|
started = "started",
|
||||||
|
status = "status",
|
||||||
|
timesUsed = "timesUsed"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VoucherListFilterOpts {
|
||||||
|
saleType: FilterOpts<VoucherDiscountType[]>;
|
||||||
|
started: FilterOpts<MinMax>;
|
||||||
|
status: FilterOpts<DiscountStatusEnum[]>;
|
||||||
|
timesUsed: FilterOpts<MinMax>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
active: {
|
||||||
|
defaultMessage: "Active",
|
||||||
|
description: "voucher status"
|
||||||
|
},
|
||||||
|
expired: {
|
||||||
|
defaultMessage: "Expired",
|
||||||
|
description: "voucher status"
|
||||||
|
},
|
||||||
|
fixed: {
|
||||||
|
defaultMessage: "Fixed amount",
|
||||||
|
description: "discount type"
|
||||||
|
},
|
||||||
|
percentage: {
|
||||||
|
defaultMessage: "Percentage",
|
||||||
|
description: "discount type"
|
||||||
|
},
|
||||||
|
scheduled: {
|
||||||
|
defaultMessage: "Scheduled",
|
||||||
|
description: "voucher status"
|
||||||
|
},
|
||||||
|
started: {
|
||||||
|
defaultMessage: "Started",
|
||||||
|
description: "voucher start date"
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
defaultMessage: "Status",
|
||||||
|
description: "voucher status"
|
||||||
|
},
|
||||||
|
timesUsed: {
|
||||||
|
defaultMessage: "Times used",
|
||||||
|
description: "voucher"
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
defaultMessage: "Discount Type"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createFilterStructure(
|
||||||
|
intl: IntlShape,
|
||||||
|
opts: VoucherListFilterOpts
|
||||||
|
): IFilter<VoucherFilterKeys> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...createDateField(
|
||||||
|
VoucherFilterKeys.started,
|
||||||
|
intl.formatMessage(messages.started),
|
||||||
|
opts.started.value
|
||||||
|
),
|
||||||
|
active: opts.started.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createNumberField(
|
||||||
|
VoucherFilterKeys.timesUsed,
|
||||||
|
intl.formatMessage(messages.timesUsed),
|
||||||
|
opts.timesUsed.value
|
||||||
|
),
|
||||||
|
active: opts.timesUsed.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createOptionsField(
|
||||||
|
VoucherFilterKeys.status,
|
||||||
|
intl.formatMessage(messages.status),
|
||||||
|
opts.status.value,
|
||||||
|
true,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.active),
|
||||||
|
value: DiscountStatusEnum.ACTIVE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.expired),
|
||||||
|
value: DiscountStatusEnum.EXPIRED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.scheduled),
|
||||||
|
value: DiscountStatusEnum.SCHEDULED
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
active: opts.status.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createOptionsField(
|
||||||
|
VoucherFilterKeys.saleType,
|
||||||
|
intl.formatMessage(messages.type),
|
||||||
|
opts.saleType.value,
|
||||||
|
false,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.fixed),
|
||||||
|
value: VoucherDiscountType.FIXED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.percentage),
|
||||||
|
value: VoucherDiscountType.PERCENTAGE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.percentage),
|
||||||
|
value: VoucherDiscountType.SHIPPING
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
active: opts.saleType.active
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
export { default } from "./VoucherListPage";
|
export { default } from "./VoucherListPage";
|
||||||
export * from "./VoucherListPage";
|
export * from "./VoucherListPage";
|
||||||
|
export * from "./filters";
|
||||||
|
|
|
@ -8,7 +8,8 @@ import {
|
||||||
Filters,
|
Filters,
|
||||||
Pagination,
|
Pagination,
|
||||||
TabActionDialog,
|
TabActionDialog,
|
||||||
Sort
|
Sort,
|
||||||
|
FiltersWithMultipleValues
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { SaleDetailsPageTab } from "./components/SaleDetailsPage";
|
import { SaleDetailsPageTab } from "./components/SaleDetailsPage";
|
||||||
import { VoucherDetailsPageTab } from "./components/VoucherDetailsPage";
|
import { VoucherDetailsPageTab } from "./components/VoucherDetailsPage";
|
||||||
|
@ -18,9 +19,16 @@ export const discountSection = "/discounts/";
|
||||||
export const saleSection = urlJoin(discountSection, "sales");
|
export const saleSection = urlJoin(discountSection, "sales");
|
||||||
export const saleListPath = saleSection;
|
export const saleListPath = saleSection;
|
||||||
export enum SaleListUrlFiltersEnum {
|
export enum SaleListUrlFiltersEnum {
|
||||||
|
type = "type",
|
||||||
|
startedFrom = "startedFrom",
|
||||||
|
startedTo = "startedTo",
|
||||||
query = "query"
|
query = "query"
|
||||||
}
|
}
|
||||||
export type SaleListUrlFilters = Filters<SaleListUrlFiltersEnum>;
|
export enum SaleListUrlFiltersWithMultipleValues {
|
||||||
|
status = "status"
|
||||||
|
}
|
||||||
|
export type SaleListUrlFilters = Filters<SaleListUrlFiltersEnum> &
|
||||||
|
FiltersWithMultipleValues<SaleListUrlFiltersWithMultipleValues>;
|
||||||
export type SaleListUrlDialog = "remove" | TabActionDialog;
|
export type SaleListUrlDialog = "remove" | TabActionDialog;
|
||||||
export enum SaleListUrlSortField {
|
export enum SaleListUrlSortField {
|
||||||
name = "name",
|
name = "name",
|
||||||
|
@ -59,9 +67,18 @@ export const saleAddUrl = saleAddPath;
|
||||||
export const voucherSection = urlJoin(discountSection, "vouchers");
|
export const voucherSection = urlJoin(discountSection, "vouchers");
|
||||||
export const voucherListPath = voucherSection;
|
export const voucherListPath = voucherSection;
|
||||||
export enum VoucherListUrlFiltersEnum {
|
export enum VoucherListUrlFiltersEnum {
|
||||||
|
startedFrom = "startedFrom",
|
||||||
|
startedTo = "startedTo",
|
||||||
|
timesUsedFrom = "timesUsedFrom",
|
||||||
|
timesUsedTo = "timesUsedTo",
|
||||||
query = "query"
|
query = "query"
|
||||||
}
|
}
|
||||||
export type VoucherListUrlFilters = Filters<VoucherListUrlFiltersEnum>;
|
export enum VoucherListUrlFiltersWithMultipleValues {
|
||||||
|
status = "status",
|
||||||
|
type = "type"
|
||||||
|
}
|
||||||
|
export type VoucherListUrlFilters = Filters<VoucherListUrlFiltersEnum> &
|
||||||
|
FiltersWithMultipleValues<VoucherListUrlFiltersWithMultipleValues>;
|
||||||
export type VoucherListUrlDialog = "remove" | TabActionDialog;
|
export type VoucherListUrlDialog = "remove" | TabActionDialog;
|
||||||
export enum VoucherListUrlSortField {
|
export enum VoucherListUrlSortField {
|
||||||
code = "code",
|
code = "code",
|
||||||
|
|
|
@ -24,13 +24,13 @@ import { ListViews } from "@saleor/types";
|
||||||
import { getSortParams } from "@saleor/utils/sort";
|
import { getSortParams } from "@saleor/utils/sort";
|
||||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||||
|
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||||
import SaleListPage from "../../components/SaleListPage";
|
import SaleListPage from "../../components/SaleListPage";
|
||||||
import { TypedSaleBulkDelete } from "../../mutations";
|
import { TypedSaleBulkDelete } from "../../mutations";
|
||||||
import { useSaleListQuery } from "../../queries";
|
import { useSaleListQuery } from "../../queries";
|
||||||
import { SaleBulkDelete } from "../../types/SaleBulkDelete";
|
import { SaleBulkDelete } from "../../types/SaleBulkDelete";
|
||||||
import {
|
import {
|
||||||
saleAddUrl,
|
saleAddUrl,
|
||||||
SaleListUrlFilters,
|
|
||||||
SaleListUrlQueryParams,
|
SaleListUrlQueryParams,
|
||||||
saleUrl,
|
saleUrl,
|
||||||
saleListUrl,
|
saleListUrl,
|
||||||
|
@ -42,8 +42,10 @@ import {
|
||||||
getActiveFilters,
|
getActiveFilters,
|
||||||
getFilterTabs,
|
getFilterTabs,
|
||||||
getFilterVariables,
|
getFilterVariables,
|
||||||
saveFilterTab
|
saveFilterTab,
|
||||||
} from "./filter";
|
getFilterQueryParam,
|
||||||
|
getFilterOpts
|
||||||
|
} from "./filters";
|
||||||
import { getSortQueryVariables } from "./sort";
|
import { getSortQueryVariables } from "./sort";
|
||||||
|
|
||||||
interface SaleListProps {
|
interface SaleListProps {
|
||||||
|
@ -86,16 +88,17 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
||||||
: 0
|
: 0
|
||||||
: parseInt(params.activeTab, 0);
|
: parseInt(params.activeTab, 0);
|
||||||
|
|
||||||
const changeFilterField = (filter: SaleListUrlFilters) => {
|
const [
|
||||||
reset();
|
changeFilters,
|
||||||
navigate(
|
resetFilters,
|
||||||
saleListUrl({
|
handleSearchChange
|
||||||
...getActiveFilters(params),
|
] = createFilterHandlers({
|
||||||
...filter,
|
cleanupFn: reset,
|
||||||
activeTab: undefined
|
createUrl: saleListUrl,
|
||||||
})
|
getFilterQueryParam,
|
||||||
);
|
navigate,
|
||||||
};
|
params
|
||||||
|
});
|
||||||
|
|
||||||
const [openModal, closeModal] = createDialogActionHandlers<
|
const [openModal, closeModal] = createDialogActionHandlers<
|
||||||
SaleListUrlDialog,
|
SaleListUrlDialog,
|
||||||
|
@ -143,6 +146,7 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSort = createSortHandler(navigate, saleListUrl, params);
|
const handleSort = createSortHandler(navigate, saleListUrl, params);
|
||||||
|
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TypedSaleBulkDelete onCompleted={handleSaleBulkDelete}>
|
<TypedSaleBulkDelete onCompleted={handleSaleBulkDelete}>
|
||||||
|
@ -158,10 +162,13 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
||||||
<>
|
<>
|
||||||
<WindowTitle title={intl.formatMessage(sectionNames.sales)} />
|
<WindowTitle title={intl.formatMessage(sectionNames.sales)} />
|
||||||
<SaleListPage
|
<SaleListPage
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
filterOpts={getFilterOpts(params)}
|
||||||
initialSearch={params.query || ""}
|
initialSearch={params.query || ""}
|
||||||
onSearchChange={query => changeFilterField({ query })}
|
onSearchChange={handleSearchChange}
|
||||||
onAll={() => navigate(saleListUrl())}
|
onFilterChange={filter => changeFilters(filter)}
|
||||||
|
onAll={resetFilters}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
onTabDelete={() => openModal("delete-search")}
|
onTabDelete={() => openModal("delete-search")}
|
||||||
onTabSave={() => openModal("save-search")}
|
onTabSave={() => openModal("save-search")}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
|
||||||
|
Object {
|
||||||
|
"startedFrom": "2019-12-09",
|
||||||
|
"startedTo": "2019-12-38",
|
||||||
|
"status": Array [
|
||||||
|
"ACTIVE",
|
||||||
|
"EXPIRED",
|
||||||
|
],
|
||||||
|
"type": "FIXED",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"startedFrom=2019-12-09&startedTo=2019-12-38&status%5B0%5D=ACTIVE&status%5B1%5D=EXPIRED&type=FIXED"`;
|
|
@ -1,31 +0,0 @@
|
||||||
import { SaleFilterInput } from "@saleor/types/globalTypes";
|
|
||||||
import {
|
|
||||||
createFilterTabUtils,
|
|
||||||
createFilterUtils
|
|
||||||
} from "../../../utils/filters";
|
|
||||||
import {
|
|
||||||
SaleListUrlFilters,
|
|
||||||
SaleListUrlFiltersEnum,
|
|
||||||
SaleListUrlQueryParams
|
|
||||||
} from "../../urls";
|
|
||||||
|
|
||||||
export const SALE_FILTERS_KEY = "saleFilters";
|
|
||||||
|
|
||||||
export function getFilterVariables(
|
|
||||||
params: SaleListUrlFilters
|
|
||||||
): SaleFilterInput {
|
|
||||||
return {
|
|
||||||
search: params.query
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const {
|
|
||||||
deleteFilterTab,
|
|
||||||
getFilterTabs,
|
|
||||||
saveFilterTab
|
|
||||||
} = createFilterTabUtils<SaleListUrlFilters>(SALE_FILTERS_KEY);
|
|
||||||
|
|
||||||
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
|
|
||||||
SaleListUrlQueryParams,
|
|
||||||
SaleListUrlFilters
|
|
||||||
>(SaleListUrlFiltersEnum);
|
|
76
src/discounts/views/SaleList/filters.test.ts
Normal file
76
src/discounts/views/SaleList/filters.test.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { createIntl } from "react-intl";
|
||||||
|
import { stringify as stringifyQs } from "qs";
|
||||||
|
|
||||||
|
import { SaleListUrlFilters } from "@saleor/discounts/urls";
|
||||||
|
import { createFilterStructure } from "@saleor/discounts/components/SaleListPage";
|
||||||
|
import { getFilterQueryParams } from "@saleor/utils/filters";
|
||||||
|
import { date } from "@saleor/fixtures";
|
||||||
|
import { getExistingKeys, setFilterOptsStatus } from "@test/filters";
|
||||||
|
import { config } from "@test/intl";
|
||||||
|
import {
|
||||||
|
DiscountValueTypeEnum,
|
||||||
|
DiscountStatusEnum
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
|
import { getFilterVariables, getFilterQueryParam } from "./filters";
|
||||||
|
|
||||||
|
describe("Filtering query params", () => {
|
||||||
|
it("should be empty object if no params given", () => {
|
||||||
|
const params: SaleListUrlFilters = {};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterVariables)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be empty object if params given", () => {
|
||||||
|
const params: SaleListUrlFilters = {
|
||||||
|
startedFrom: date.from,
|
||||||
|
startedTo: date.to,
|
||||||
|
status: [DiscountStatusEnum.ACTIVE, DiscountStatusEnum.EXPIRED],
|
||||||
|
type: DiscountValueTypeEnum.FIXED
|
||||||
|
};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterVariables)).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Filtering URL params", () => {
|
||||||
|
const intl = createIntl(config);
|
||||||
|
|
||||||
|
const filters = createFilterStructure(intl, {
|
||||||
|
saleType: {
|
||||||
|
active: false,
|
||||||
|
value: DiscountValueTypeEnum.FIXED
|
||||||
|
},
|
||||||
|
started: {
|
||||||
|
active: false,
|
||||||
|
value: {
|
||||||
|
max: date.to,
|
||||||
|
min: date.from
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
active: false,
|
||||||
|
value: [DiscountStatusEnum.ACTIVE, DiscountStatusEnum.EXPIRED]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be empty if no active filters", () => {
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
filters,
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterQueryParams)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be empty if active filters are present", () => {
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
setFilterOptsStatus(filters, true),
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(filterQueryParams).toMatchSnapshot();
|
||||||
|
expect(stringifyQs(filterQueryParams)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
119
src/discounts/views/SaleList/filters.ts
Normal file
119
src/discounts/views/SaleList/filters.ts
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import {
|
||||||
|
DiscountStatusEnum,
|
||||||
|
DiscountValueTypeEnum,
|
||||||
|
SaleFilterInput
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
|
import { maybe, findValueInEnum, joinDateTime } from "@saleor/misc";
|
||||||
|
import { IFilterElement } from "@saleor/components/Filter";
|
||||||
|
import {
|
||||||
|
SaleListFilterOpts,
|
||||||
|
SaleFilterKeys
|
||||||
|
} from "@saleor/discounts/components/SaleListPage";
|
||||||
|
import {
|
||||||
|
createFilterTabUtils,
|
||||||
|
createFilterUtils,
|
||||||
|
dedupeFilter,
|
||||||
|
getGteLteVariables,
|
||||||
|
getSingleEnumValueQueryParam,
|
||||||
|
getMinMaxQueryParam,
|
||||||
|
getMultipleEnumValueQueryParam
|
||||||
|
} from "../../../utils/filters";
|
||||||
|
import {
|
||||||
|
SaleListUrlFilters,
|
||||||
|
SaleListUrlFiltersEnum,
|
||||||
|
SaleListUrlQueryParams,
|
||||||
|
SaleListUrlFiltersWithMultipleValues
|
||||||
|
} from "../../urls";
|
||||||
|
|
||||||
|
export const SALE_FILTERS_KEY = "saleFilters";
|
||||||
|
|
||||||
|
export function getFilterOpts(params: SaleListUrlFilters): SaleListFilterOpts {
|
||||||
|
return {
|
||||||
|
saleType: {
|
||||||
|
active: !!maybe(() => params.type),
|
||||||
|
value: maybe(() => findValueInEnum(params.type, DiscountValueTypeEnum))
|
||||||
|
},
|
||||||
|
started: {
|
||||||
|
active: maybe(
|
||||||
|
() =>
|
||||||
|
[params.startedFrom, params.startedTo].some(
|
||||||
|
field => field !== undefined
|
||||||
|
),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
max: maybe(() => params.startedTo, ""),
|
||||||
|
min: maybe(() => params.startedFrom, "")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
active: !!maybe(() => params.status),
|
||||||
|
value: maybe(
|
||||||
|
() =>
|
||||||
|
dedupeFilter(
|
||||||
|
params.status.map(status =>
|
||||||
|
findValueInEnum(status, DiscountStatusEnum)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilterVariables(
|
||||||
|
params: SaleListUrlFilters
|
||||||
|
): SaleFilterInput {
|
||||||
|
return {
|
||||||
|
saleType:
|
||||||
|
params.type && findValueInEnum(params.type, DiscountValueTypeEnum),
|
||||||
|
search: params.query,
|
||||||
|
started: getGteLteVariables({
|
||||||
|
gte: joinDateTime(params.startedFrom),
|
||||||
|
lte: joinDateTime(params.startedTo)
|
||||||
|
}),
|
||||||
|
status:
|
||||||
|
params.status &&
|
||||||
|
params.status.map(status => findValueInEnum(status, DiscountStatusEnum))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilterQueryParam(
|
||||||
|
filter: IFilterElement<SaleFilterKeys>
|
||||||
|
): SaleListUrlFilters {
|
||||||
|
const { name } = filter;
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case SaleFilterKeys.saleType:
|
||||||
|
return getSingleEnumValueQueryParam(
|
||||||
|
filter,
|
||||||
|
SaleListUrlFiltersEnum.type,
|
||||||
|
DiscountValueTypeEnum
|
||||||
|
);
|
||||||
|
|
||||||
|
case SaleFilterKeys.started:
|
||||||
|
return getMinMaxQueryParam(
|
||||||
|
filter,
|
||||||
|
SaleListUrlFiltersEnum.startedFrom,
|
||||||
|
SaleListUrlFiltersEnum.startedTo
|
||||||
|
);
|
||||||
|
|
||||||
|
case SaleFilterKeys.status:
|
||||||
|
return getMultipleEnumValueQueryParam(
|
||||||
|
filter,
|
||||||
|
SaleListUrlFiltersWithMultipleValues.status,
|
||||||
|
DiscountStatusEnum
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const {
|
||||||
|
deleteFilterTab,
|
||||||
|
getFilterTabs,
|
||||||
|
saveFilterTab
|
||||||
|
} = createFilterTabUtils<SaleListUrlFilters>(SALE_FILTERS_KEY);
|
||||||
|
|
||||||
|
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
|
||||||
|
SaleListUrlQueryParams,
|
||||||
|
SaleListUrlFilters
|
||||||
|
>(SaleListUrlFiltersEnum);
|
|
@ -24,6 +24,7 @@ import { ListViews } from "@saleor/types";
|
||||||
import { getSortParams } from "@saleor/utils/sort";
|
import { getSortParams } from "@saleor/utils/sort";
|
||||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||||
|
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||||
import VoucherListPage from "../../components/VoucherListPage";
|
import VoucherListPage from "../../components/VoucherListPage";
|
||||||
import { TypedVoucherBulkDelete } from "../../mutations";
|
import { TypedVoucherBulkDelete } from "../../mutations";
|
||||||
import { useVoucherListQuery } from "../../queries";
|
import { useVoucherListQuery } from "../../queries";
|
||||||
|
@ -31,7 +32,6 @@ import { VoucherBulkDelete } from "../../types/VoucherBulkDelete";
|
||||||
import {
|
import {
|
||||||
voucherAddUrl,
|
voucherAddUrl,
|
||||||
voucherListUrl,
|
voucherListUrl,
|
||||||
VoucherListUrlFilters,
|
|
||||||
VoucherListUrlQueryParams,
|
VoucherListUrlQueryParams,
|
||||||
voucherUrl,
|
voucherUrl,
|
||||||
VoucherListUrlDialog
|
VoucherListUrlDialog
|
||||||
|
@ -42,8 +42,10 @@ import {
|
||||||
getActiveFilters,
|
getActiveFilters,
|
||||||
getFilterTabs,
|
getFilterTabs,
|
||||||
getFilterVariables,
|
getFilterVariables,
|
||||||
saveFilterTab
|
saveFilterTab,
|
||||||
} from "./filter";
|
getFilterQueryParam,
|
||||||
|
getFilterOpts
|
||||||
|
} from "./filters";
|
||||||
import { getSortQueryVariables } from "./sort";
|
import { getSortQueryVariables } from "./sort";
|
||||||
|
|
||||||
interface VoucherListProps {
|
interface VoucherListProps {
|
||||||
|
@ -86,16 +88,17 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
|
||||||
: 0
|
: 0
|
||||||
: parseInt(params.activeTab, 0);
|
: parseInt(params.activeTab, 0);
|
||||||
|
|
||||||
const changeFilterField = (filter: VoucherListUrlFilters) => {
|
const [
|
||||||
reset();
|
changeFilters,
|
||||||
navigate(
|
resetFilters,
|
||||||
voucherListUrl({
|
handleSearchChange
|
||||||
...getActiveFilters(params),
|
] = createFilterHandlers({
|
||||||
...filter,
|
cleanupFn: reset,
|
||||||
activeTab: undefined
|
createUrl: voucherListUrl,
|
||||||
})
|
getFilterQueryParam,
|
||||||
);
|
navigate,
|
||||||
};
|
params
|
||||||
|
});
|
||||||
|
|
||||||
const [openModal, closeModal] = createDialogActionHandlers<
|
const [openModal, closeModal] = createDialogActionHandlers<
|
||||||
VoucherListUrlDialog,
|
VoucherListUrlDialog,
|
||||||
|
@ -143,6 +146,7 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSort = createSortHandler(navigate, voucherListUrl, params);
|
const handleSort = createSortHandler(navigate, voucherListUrl, params);
|
||||||
|
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TypedVoucherBulkDelete onCompleted={handleVoucherBulkDelete}>
|
<TypedVoucherBulkDelete onCompleted={handleVoucherBulkDelete}>
|
||||||
|
@ -158,10 +162,13 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
|
||||||
<>
|
<>
|
||||||
<WindowTitle title={intl.formatMessage(sectionNames.vouchers)} />
|
<WindowTitle title={intl.formatMessage(sectionNames.vouchers)} />
|
||||||
<VoucherListPage
|
<VoucherListPage
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
filterOpts={getFilterOpts(params)}
|
||||||
initialSearch={params.query || ""}
|
initialSearch={params.query || ""}
|
||||||
onSearchChange={query => changeFilterField({ query })}
|
onSearchChange={handleSearchChange}
|
||||||
onAll={() => navigate(voucherListUrl())}
|
onFilterChange={filter => changeFilters(filter)}
|
||||||
|
onAll={resetFilters}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
onTabDelete={() => openModal("delete-search")}
|
onTabDelete={() => openModal("delete-search")}
|
||||||
onTabSave={() => openModal("save-search")}
|
onTabSave={() => openModal("save-search")}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
|
||||||
|
Object {
|
||||||
|
"startedFrom": "2019-12-09",
|
||||||
|
"startedTo": "2019-12-38",
|
||||||
|
"status": Array [
|
||||||
|
"ACTIVE",
|
||||||
|
"EXPIRED",
|
||||||
|
],
|
||||||
|
"timesUsedFrom": "1",
|
||||||
|
"timesUsedTo": "6",
|
||||||
|
"type": Array [
|
||||||
|
"FIXED",
|
||||||
|
"SHIPPING",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"startedFrom=2019-12-09&startedTo=2019-12-38×UsedFrom=1×UsedTo=6&status%5B0%5D=ACTIVE&status%5B1%5D=EXPIRED&type%5B0%5D=FIXED&type%5B1%5D=SHIPPING"`;
|
|
@ -1,31 +0,0 @@
|
||||||
import { VoucherFilterInput } from "@saleor/types/globalTypes";
|
|
||||||
import {
|
|
||||||
createFilterTabUtils,
|
|
||||||
createFilterUtils
|
|
||||||
} from "../../../utils/filters";
|
|
||||||
import {
|
|
||||||
VoucherListUrlFilters,
|
|
||||||
VoucherListUrlFiltersEnum,
|
|
||||||
VoucherListUrlQueryParams
|
|
||||||
} from "../../urls";
|
|
||||||
|
|
||||||
export const VOUCHER_FILTERS_KEY = "VoucherFilters";
|
|
||||||
|
|
||||||
export function getFilterVariables(
|
|
||||||
params: VoucherListUrlFilters
|
|
||||||
): VoucherFilterInput {
|
|
||||||
return {
|
|
||||||
search: params.query
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const {
|
|
||||||
deleteFilterTab,
|
|
||||||
getFilterTabs,
|
|
||||||
saveFilterTab
|
|
||||||
} = createFilterTabUtils<VoucherListUrlFilters>(VOUCHER_FILTERS_KEY);
|
|
||||||
|
|
||||||
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
|
|
||||||
VoucherListUrlQueryParams,
|
|
||||||
VoucherListUrlFilters
|
|
||||||
>(VoucherListUrlFiltersEnum);
|
|
85
src/discounts/views/VoucherList/filters.test.ts
Normal file
85
src/discounts/views/VoucherList/filters.test.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import { createIntl } from "react-intl";
|
||||||
|
import { stringify as stringifyQs } from "qs";
|
||||||
|
|
||||||
|
import { VoucherListUrlFilters } from "@saleor/discounts/urls";
|
||||||
|
import { createFilterStructure } from "@saleor/discounts/components/VoucherListPage";
|
||||||
|
import { getFilterQueryParams } from "@saleor/utils/filters";
|
||||||
|
import { date } from "@saleor/fixtures";
|
||||||
|
import { getExistingKeys, setFilterOptsStatus } from "@test/filters";
|
||||||
|
import { config } from "@test/intl";
|
||||||
|
import {
|
||||||
|
DiscountStatusEnum,
|
||||||
|
VoucherDiscountType
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
|
import { getFilterVariables, getFilterQueryParam } from "./filters";
|
||||||
|
|
||||||
|
describe("Filtering query params", () => {
|
||||||
|
it("should be empty object if no params given", () => {
|
||||||
|
const params: VoucherListUrlFilters = {};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterVariables)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be empty object if params given", () => {
|
||||||
|
const params: VoucherListUrlFilters = {
|
||||||
|
startedFrom: date.from,
|
||||||
|
startedTo: date.to,
|
||||||
|
status: [DiscountStatusEnum.ACTIVE, DiscountStatusEnum.EXPIRED],
|
||||||
|
timesUsedFrom: date.from,
|
||||||
|
timesUsedTo: date.to,
|
||||||
|
type: [VoucherDiscountType.FIXED, VoucherDiscountType.SHIPPING]
|
||||||
|
};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterVariables)).toHaveLength(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Filtering URL params", () => {
|
||||||
|
const intl = createIntl(config);
|
||||||
|
|
||||||
|
const filters = createFilterStructure(intl, {
|
||||||
|
saleType: {
|
||||||
|
active: false,
|
||||||
|
value: [VoucherDiscountType.FIXED, VoucherDiscountType.SHIPPING]
|
||||||
|
},
|
||||||
|
started: {
|
||||||
|
active: false,
|
||||||
|
value: {
|
||||||
|
max: date.to,
|
||||||
|
min: date.from
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
active: false,
|
||||||
|
value: [DiscountStatusEnum.ACTIVE, DiscountStatusEnum.EXPIRED]
|
||||||
|
},
|
||||||
|
timesUsed: {
|
||||||
|
active: false,
|
||||||
|
value: {
|
||||||
|
max: "6",
|
||||||
|
min: "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be empty if no active filters", () => {
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
filters,
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterQueryParams)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be empty if active filters are present", () => {
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
setFilterOptsStatus(filters, true),
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(filterQueryParams).toMatchSnapshot();
|
||||||
|
expect(stringifyQs(filterQueryParams)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
151
src/discounts/views/VoucherList/filters.ts
Normal file
151
src/discounts/views/VoucherList/filters.ts
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
import {
|
||||||
|
VoucherFilterInput,
|
||||||
|
DiscountStatusEnum,
|
||||||
|
VoucherDiscountType
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
|
import { maybe, findValueInEnum, joinDateTime } from "@saleor/misc";
|
||||||
|
import { IFilterElement } from "@saleor/components/Filter";
|
||||||
|
import {
|
||||||
|
VoucherListFilterOpts,
|
||||||
|
VoucherFilterKeys
|
||||||
|
} from "@saleor/discounts/components/VoucherListPage";
|
||||||
|
import {
|
||||||
|
createFilterTabUtils,
|
||||||
|
createFilterUtils,
|
||||||
|
dedupeFilter,
|
||||||
|
getGteLteVariables,
|
||||||
|
getMultipleEnumValueQueryParam,
|
||||||
|
getMinMaxQueryParam
|
||||||
|
} from "../../../utils/filters";
|
||||||
|
import {
|
||||||
|
VoucherListUrlFilters,
|
||||||
|
VoucherListUrlFiltersEnum,
|
||||||
|
VoucherListUrlQueryParams,
|
||||||
|
VoucherListUrlFiltersWithMultipleValues
|
||||||
|
} from "../../urls";
|
||||||
|
|
||||||
|
export const VOUCHER_FILTERS_KEY = "voucherFilters";
|
||||||
|
|
||||||
|
export function getFilterOpts(
|
||||||
|
params: VoucherListUrlFilters
|
||||||
|
): VoucherListFilterOpts {
|
||||||
|
return {
|
||||||
|
saleType: {
|
||||||
|
active: !!maybe(() => params.type),
|
||||||
|
value: maybe(
|
||||||
|
() =>
|
||||||
|
dedupeFilter(
|
||||||
|
params.type.map(type => findValueInEnum(type, VoucherDiscountType))
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
started: {
|
||||||
|
active: maybe(
|
||||||
|
() =>
|
||||||
|
[params.startedFrom, params.startedTo].some(
|
||||||
|
field => field !== undefined
|
||||||
|
),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
max: maybe(() => params.startedTo, ""),
|
||||||
|
min: maybe(() => params.startedFrom, "")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
active: !!maybe(() => params.status),
|
||||||
|
value: maybe(
|
||||||
|
() =>
|
||||||
|
dedupeFilter(
|
||||||
|
params.status.map(status =>
|
||||||
|
findValueInEnum(status, DiscountStatusEnum)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
timesUsed: {
|
||||||
|
active: maybe(
|
||||||
|
() =>
|
||||||
|
[params.timesUsedFrom, params.timesUsedTo].some(
|
||||||
|
field => field !== undefined
|
||||||
|
),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
max: maybe(() => params.timesUsedTo, ""),
|
||||||
|
min: maybe(() => params.timesUsedFrom, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilterVariables(
|
||||||
|
params: VoucherListUrlFilters
|
||||||
|
): VoucherFilterInput {
|
||||||
|
return {
|
||||||
|
discountType:
|
||||||
|
params.type &&
|
||||||
|
params.type.map(type => findValueInEnum(type, VoucherDiscountType)),
|
||||||
|
search: params.query,
|
||||||
|
started: getGteLteVariables({
|
||||||
|
gte: joinDateTime(params.startedFrom),
|
||||||
|
lte: joinDateTime(params.startedTo)
|
||||||
|
}),
|
||||||
|
status:
|
||||||
|
params.status &&
|
||||||
|
params.status.map(status => findValueInEnum(status, DiscountStatusEnum)),
|
||||||
|
timesUsed: getGteLteVariables({
|
||||||
|
gte: parseInt(params.timesUsedFrom, 0),
|
||||||
|
lte: parseInt(params.timesUsedTo, 0)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilterQueryParam(
|
||||||
|
filter: IFilterElement<VoucherFilterKeys>
|
||||||
|
): VoucherListUrlFilters {
|
||||||
|
const { name } = filter;
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case VoucherFilterKeys.saleType:
|
||||||
|
return getMultipleEnumValueQueryParam(
|
||||||
|
filter,
|
||||||
|
VoucherListUrlFiltersWithMultipleValues.type,
|
||||||
|
VoucherDiscountType
|
||||||
|
);
|
||||||
|
|
||||||
|
case VoucherFilterKeys.started:
|
||||||
|
return getMinMaxQueryParam(
|
||||||
|
filter,
|
||||||
|
VoucherListUrlFiltersEnum.startedFrom,
|
||||||
|
VoucherListUrlFiltersEnum.startedTo
|
||||||
|
);
|
||||||
|
|
||||||
|
case VoucherFilterKeys.timesUsed:
|
||||||
|
return getMinMaxQueryParam(
|
||||||
|
filter,
|
||||||
|
VoucherListUrlFiltersEnum.timesUsedFrom,
|
||||||
|
VoucherListUrlFiltersEnum.timesUsedTo
|
||||||
|
);
|
||||||
|
|
||||||
|
case VoucherFilterKeys.status:
|
||||||
|
return getMultipleEnumValueQueryParam(
|
||||||
|
filter,
|
||||||
|
VoucherListUrlFiltersWithMultipleValues.status,
|
||||||
|
DiscountStatusEnum
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const {
|
||||||
|
deleteFilterTab,
|
||||||
|
getFilterTabs,
|
||||||
|
saveFilterTab
|
||||||
|
} = createFilterTabUtils<VoucherListUrlFilters>(VOUCHER_FILTERS_KEY);
|
||||||
|
|
||||||
|
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
|
||||||
|
VoucherListUrlQueryParams,
|
||||||
|
VoucherListUrlFilters
|
||||||
|
>(VoucherListUrlFiltersEnum);
|
|
@ -1,5 +1,4 @@
|
||||||
import { ShopInfo_shop_permissions } from "./components/Shop/types/ShopInfo";
|
import { ShopInfo_shop_permissions } from "./components/Shop/types/ShopInfo";
|
||||||
import { Filter } from "./components/TableFilter";
|
|
||||||
import {
|
import {
|
||||||
FetchMoreProps,
|
FetchMoreProps,
|
||||||
FilterPageProps,
|
FilterPageProps,
|
||||||
|
@ -303,80 +302,14 @@ export const searchPageProps: SearchPageProps = {
|
||||||
onSearchChange: () => undefined
|
onSearchChange: () => undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterPageProps: FilterPageProps = {
|
export const filterPageProps: FilterPageProps<string, object> = {
|
||||||
...searchPageProps,
|
...searchPageProps,
|
||||||
...tabPageProps,
|
...tabPageProps,
|
||||||
currencySymbol: "USD",
|
currencySymbol: "USD",
|
||||||
filtersList: [],
|
filterOpts: {},
|
||||||
onFilterAdd: () => undefined
|
onFilterChange: () => undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filters: Filter[] = [
|
|
||||||
{
|
|
||||||
label: "Property X is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property Y is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property Z is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property X is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property Y is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property Z is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property X is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property Y is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property Z is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property X is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property Y is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property Z is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property X is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property Y is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Property Z is ",
|
|
||||||
onClick: () => undefined
|
|
||||||
}
|
|
||||||
].map((filter, filterIndex) => ({
|
|
||||||
...filter,
|
|
||||||
label: filter.label + filterIndex
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const fetchMoreProps: FetchMoreProps = {
|
export const fetchMoreProps: FetchMoreProps = {
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -443,3 +376,8 @@ export const permissions: ShopInfo_shop_permissions[] = [
|
||||||
__typename: "PermissionDisplay" as "PermissionDisplay",
|
__typename: "PermissionDisplay" as "PermissionDisplay",
|
||||||
...perm
|
...perm
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const date = {
|
||||||
|
from: "2019-12-09",
|
||||||
|
to: "2019-12-38"
|
||||||
|
};
|
||||||
|
|
|
@ -48,7 +48,7 @@ const HomeSection = () => {
|
||||||
onProductsOutOfStockClick={() =>
|
onProductsOutOfStockClick={() =>
|
||||||
navigate(
|
navigate(
|
||||||
productListUrl({
|
productListUrl({
|
||||||
status: StockAvailability.OUT_OF_STOCK
|
stockStatus: StockAvailability.OUT_OF_STOCK
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
8
src/hooks/debug/useOnMount.ts
Normal file
8
src/hooks/debug/useOnMount.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
function useOnMount(name: string) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
useEffect(() => console.log(`mounted node ${name}`), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useOnMount;
|
|
@ -65,6 +65,9 @@ export const commonMessages = defineMessages({
|
||||||
startHour: {
|
startHour: {
|
||||||
defaultMessage: "Start Hour"
|
defaultMessage: "Start Hour"
|
||||||
},
|
},
|
||||||
|
status: {
|
||||||
|
defaultMessage: "Status"
|
||||||
|
},
|
||||||
summary: {
|
summary: {
|
||||||
defaultMessage: "Summary"
|
defaultMessage: "Summary"
|
||||||
},
|
},
|
||||||
|
|
10
src/misc.ts
10
src/misc.ts
|
@ -115,7 +115,7 @@ export const transformPaymentStatus = (status: string, intl: IntlShape) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const orderStatusMessages = defineMessages({
|
export const orderStatusMessages = defineMessages({
|
||||||
cancelled: {
|
cancelled: {
|
||||||
defaultMessage: "Cancelled",
|
defaultMessage: "Cancelled",
|
||||||
description: "order status"
|
description: "order status"
|
||||||
|
@ -351,14 +351,14 @@ export function findInEnum<TEnum extends object>(
|
||||||
export function findValueInEnum<TEnum extends object>(
|
export function findValueInEnum<TEnum extends object>(
|
||||||
needle: string,
|
needle: string,
|
||||||
haystack: TEnum
|
haystack: TEnum
|
||||||
) {
|
): TEnum[keyof TEnum] {
|
||||||
const match = Object.entries(haystack).find(([_, value]) => value === needle);
|
const match = Object.entries(haystack).find(([_, value]) => value === needle);
|
||||||
|
|
||||||
if (!!match) {
|
if (!match) {
|
||||||
return match[1] as TEnum;
|
throw new Error(`Value ${needle} not found in enum`);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Value ${needle} not found in enum`);
|
return (needle as unknown) as TEnum[keyof TEnum];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseBoolean(a: string, defaultValue: boolean): boolean {
|
export function parseBoolean(a: string, defaultValue: boolean): boolean {
|
||||||
|
|
|
@ -6,34 +6,42 @@ import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import Container from "@saleor/components/Container";
|
import Container from "@saleor/components/Container";
|
||||||
import PageHeader from "@saleor/components/PageHeader";
|
import PageHeader from "@saleor/components/PageHeader";
|
||||||
import SearchBar from "@saleor/components/SearchBar";
|
|
||||||
import { sectionNames } from "@saleor/intl";
|
import { sectionNames } from "@saleor/intl";
|
||||||
import {
|
import {
|
||||||
ListActions,
|
ListActions,
|
||||||
PageListProps,
|
PageListProps,
|
||||||
SearchPageProps,
|
|
||||||
TabPageProps,
|
TabPageProps,
|
||||||
SortPage
|
SortPage,
|
||||||
|
FilterPageProps
|
||||||
} from "@saleor/types";
|
} from "@saleor/types";
|
||||||
import { OrderDraftListUrlSortField } from "@saleor/orders/urls";
|
import { OrderDraftListUrlSortField } from "@saleor/orders/urls";
|
||||||
import { OrderDraftList_draftOrders_edges_node } from "../../types/OrderDraftList";
|
import FilterBar from "@saleor/components/FilterBar";
|
||||||
import OrderDraftList from "../OrderDraftList";
|
import OrderDraftList from "../OrderDraftList";
|
||||||
|
import { OrderDraftList_draftOrders_edges_node } from "../../types/OrderDraftList";
|
||||||
|
import {
|
||||||
|
OrderDraftListFilterOpts,
|
||||||
|
OrderDraftFilterKeys,
|
||||||
|
createFilterStructure
|
||||||
|
} from "./filters";
|
||||||
|
|
||||||
export interface OrderDraftListPageProps
|
export interface OrderDraftListPageProps
|
||||||
extends PageListProps,
|
extends PageListProps,
|
||||||
ListActions,
|
ListActions,
|
||||||
SearchPageProps,
|
FilterPageProps<OrderDraftFilterKeys, OrderDraftListFilterOpts>,
|
||||||
SortPage<OrderDraftListUrlSortField>,
|
SortPage<OrderDraftListUrlSortField>,
|
||||||
TabPageProps {
|
TabPageProps {
|
||||||
orders: OrderDraftList_draftOrders_edges_node[];
|
orders: OrderDraftList_draftOrders_edges_node[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const OrderDraftListPage: React.FC<OrderDraftListPageProps> = ({
|
const OrderDraftListPage: React.FC<OrderDraftListPageProps> = ({
|
||||||
|
currencySymbol,
|
||||||
currentTab,
|
currentTab,
|
||||||
disabled,
|
disabled,
|
||||||
|
filterOpts,
|
||||||
initialSearch,
|
initialSearch,
|
||||||
onAdd,
|
onAdd,
|
||||||
onAll,
|
onAll,
|
||||||
|
onFilterChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
onTabDelete,
|
onTabDelete,
|
||||||
|
@ -43,6 +51,8 @@ const OrderDraftListPage: React.FC<OrderDraftListPageProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const structure = createFilterStructure(intl, filterOpts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<PageHeader title={intl.formatMessage(sectionNames.draftOrders)}>
|
<PageHeader title={intl.formatMessage(sectionNames.draftOrders)}>
|
||||||
|
@ -59,18 +69,21 @@ const OrderDraftListPage: React.FC<OrderDraftListPageProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Card>
|
<Card>
|
||||||
<SearchBar
|
<FilterBar
|
||||||
allTabLabel={intl.formatMessage({
|
allTabLabel={intl.formatMessage({
|
||||||
defaultMessage: "All Drafts",
|
defaultMessage: "All Drafts",
|
||||||
description: "tab name"
|
description: "tab name"
|
||||||
})}
|
})}
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
filterStructure={structure}
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
searchPlaceholder={intl.formatMessage({
|
searchPlaceholder={intl.formatMessage({
|
||||||
defaultMessage: "Search Draft"
|
defaultMessage: "Search Draft"
|
||||||
})}
|
})}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
onAll={onAll}
|
onAll={onAll}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
onTabChange={onTabChange}
|
onTabChange={onTabChange}
|
||||||
onTabDelete={onTabDelete}
|
onTabDelete={onTabDelete}
|
||||||
|
|
50
src/orders/components/OrderDraftListPage/filters.ts
Normal file
50
src/orders/components/OrderDraftListPage/filters.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { IntlShape, defineMessages } from "react-intl";
|
||||||
|
|
||||||
|
import { FilterOpts, MinMax } from "@saleor/types";
|
||||||
|
import { createDateField, createTextField } from "@saleor/utils/filters/fields";
|
||||||
|
import { IFilter } from "@saleor/components/Filter";
|
||||||
|
|
||||||
|
export enum OrderDraftFilterKeys {
|
||||||
|
created = "created",
|
||||||
|
customer = "customer"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderDraftListFilterOpts {
|
||||||
|
created: FilterOpts<MinMax>;
|
||||||
|
customer: FilterOpts<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
created: {
|
||||||
|
defaultMessage: "Created",
|
||||||
|
description: "draft order"
|
||||||
|
},
|
||||||
|
customer: {
|
||||||
|
defaultMessage: "Customer",
|
||||||
|
description: "draft order"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createFilterStructure(
|
||||||
|
intl: IntlShape,
|
||||||
|
opts: OrderDraftListFilterOpts
|
||||||
|
): IFilter<OrderDraftFilterKeys> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...createDateField(
|
||||||
|
OrderDraftFilterKeys.created,
|
||||||
|
intl.formatMessage(messages.created),
|
||||||
|
opts.created.value
|
||||||
|
),
|
||||||
|
active: opts.created.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createTextField(
|
||||||
|
OrderDraftFilterKeys.customer,
|
||||||
|
intl.formatMessage(messages.customer),
|
||||||
|
opts.customer.value
|
||||||
|
),
|
||||||
|
active: opts.customer.active
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
export { default } from "./OrderDraftListPage";
|
export { default } from "./OrderDraftListPage";
|
||||||
export * from "./OrderDraftListPage";
|
export * from "./OrderDraftListPage";
|
||||||
|
export * from "./filters";
|
||||||
|
|
|
@ -1,169 +0,0 @@
|
||||||
import moment from "moment-timezone";
|
|
||||||
import React from "react";
|
|
||||||
import { useIntl } from "react-intl";
|
|
||||||
|
|
||||||
import { DateContext } from "@saleor/components/Date/DateContext";
|
|
||||||
import { FieldType, IFilter } from "@saleor/components/Filter";
|
|
||||||
import FilterBar from "@saleor/components/FilterBar";
|
|
||||||
import TimezoneContext from "@saleor/components/Timezone";
|
|
||||||
import { FilterProps } from "../../../types";
|
|
||||||
import { OrderStatusFilter } from "../../../types/globalTypes";
|
|
||||||
|
|
||||||
type OrderListFilterProps = FilterProps<OrderFilterKeys>;
|
|
||||||
|
|
||||||
export enum OrderFilterKeys {
|
|
||||||
date = "date",
|
|
||||||
dateEqual = "dateEqual",
|
|
||||||
dateRange = "dateRange",
|
|
||||||
dateLastWeek = "dateLastWeek",
|
|
||||||
dateLastMonth = "dateLastMonth",
|
|
||||||
dateLastYear = "dateLastYear",
|
|
||||||
status = "status"
|
|
||||||
}
|
|
||||||
|
|
||||||
const OrderListFilter: React.FC<OrderListFilterProps> = props => {
|
|
||||||
const date = React.useContext(DateContext);
|
|
||||||
const tz = React.useContext(TimezoneContext);
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const filterMenu: IFilter<OrderFilterKeys> = [
|
|
||||||
{
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
children: [],
|
|
||||||
data: {
|
|
||||||
fieldLabel: null,
|
|
||||||
type: FieldType.hidden,
|
|
||||||
value: (tz ? moment(date).tz(tz) : moment(date))
|
|
||||||
.subtract(7, "days")
|
|
||||||
.toISOString()
|
|
||||||
.split("T")[0] // Remove timezone
|
|
||||||
},
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Last 7 Days"
|
|
||||||
}),
|
|
||||||
value: OrderFilterKeys.dateLastWeek
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: [],
|
|
||||||
data: {
|
|
||||||
fieldLabel: null,
|
|
||||||
type: FieldType.hidden,
|
|
||||||
value: (tz ? moment(date).tz(tz) : moment(date))
|
|
||||||
.subtract(30, "days")
|
|
||||||
.toISOString()
|
|
||||||
.split("T")[0] // Remove timezone
|
|
||||||
},
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Last 30 Days"
|
|
||||||
}),
|
|
||||||
value: OrderFilterKeys.dateLastMonth
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: [],
|
|
||||||
data: {
|
|
||||||
fieldLabel: null,
|
|
||||||
type: FieldType.hidden,
|
|
||||||
value: (tz ? moment(date).tz(tz) : moment(date))
|
|
||||||
.subtract(1, "years")
|
|
||||||
.toISOString()
|
|
||||||
.split("T")[0] // Remove timezone
|
|
||||||
},
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Last Year"
|
|
||||||
}),
|
|
||||||
value: OrderFilterKeys.dateLastYear
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: [],
|
|
||||||
data: {
|
|
||||||
additionalText: intl.formatMessage({
|
|
||||||
defaultMessage: "equals"
|
|
||||||
}),
|
|
||||||
fieldLabel: null,
|
|
||||||
type: FieldType.date
|
|
||||||
},
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Specific Date"
|
|
||||||
}),
|
|
||||||
value: OrderFilterKeys.dateEqual
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: [],
|
|
||||||
data: {
|
|
||||||
fieldLabel: intl.formatMessage({
|
|
||||||
defaultMessage: "Range"
|
|
||||||
}),
|
|
||||||
type: FieldType.rangeDate
|
|
||||||
},
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Range"
|
|
||||||
}),
|
|
||||||
value: OrderFilterKeys.dateRange
|
|
||||||
}
|
|
||||||
],
|
|
||||||
data: {
|
|
||||||
fieldLabel: intl.formatMessage({
|
|
||||||
defaultMessage: "Date"
|
|
||||||
}),
|
|
||||||
type: FieldType.select
|
|
||||||
},
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Date"
|
|
||||||
}),
|
|
||||||
value: OrderFilterKeys.date
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: [],
|
|
||||||
data: {
|
|
||||||
additionalText: intl.formatMessage({
|
|
||||||
defaultMessage: "is set as",
|
|
||||||
description: "date is set as"
|
|
||||||
}),
|
|
||||||
fieldLabel: intl.formatMessage({
|
|
||||||
defaultMessage: "Status",
|
|
||||||
description: "order fulfillment status"
|
|
||||||
}),
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Fulfilled",
|
|
||||||
description: "order fulfillment status"
|
|
||||||
}),
|
|
||||||
value: OrderStatusFilter.FULFILLED.toString()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Partially Fulfilled",
|
|
||||||
description: "order fulfillment status"
|
|
||||||
}),
|
|
||||||
value: OrderStatusFilter.PARTIALLY_FULFILLED.toString()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Unfulfilled",
|
|
||||||
description: "order fulfillment status"
|
|
||||||
}),
|
|
||||||
value: OrderStatusFilter.UNFULFILLED.toString()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Ready to Capture",
|
|
||||||
description: "order status"
|
|
||||||
}),
|
|
||||||
value: OrderStatusFilter.READY_TO_CAPTURE.toString()
|
|
||||||
}
|
|
||||||
],
|
|
||||||
type: FieldType.select
|
|
||||||
},
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Order Status"
|
|
||||||
}),
|
|
||||||
value: OrderFilterKeys.status
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return <FilterBar {...props} filterMenu={filterMenu} />;
|
|
||||||
};
|
|
||||||
OrderListFilter.displayName = "OrderListFilter";
|
|
||||||
export default OrderListFilter;
|
|
|
@ -1,2 +0,0 @@
|
||||||
export { default } from "./OrderListFilter";
|
|
||||||
export * from "./OrderListFilter";
|
|
|
@ -14,14 +14,19 @@ import {
|
||||||
SortPage
|
SortPage
|
||||||
} from "@saleor/types";
|
} from "@saleor/types";
|
||||||
import { OrderListUrlSortField } from "@saleor/orders/urls";
|
import { OrderListUrlSortField } from "@saleor/orders/urls";
|
||||||
|
import FilterBar from "@saleor/components/FilterBar";
|
||||||
import { OrderList_orders_edges_node } from "../../types/OrderList";
|
import { OrderList_orders_edges_node } from "../../types/OrderList";
|
||||||
import OrderList from "../OrderList";
|
import OrderList from "../OrderList";
|
||||||
import OrderListFilter, { OrderFilterKeys } from "../OrderListFilter";
|
import {
|
||||||
|
createFilterStructure,
|
||||||
|
OrderListFilterOpts,
|
||||||
|
OrderFilterKeys
|
||||||
|
} from "./filters";
|
||||||
|
|
||||||
export interface OrderListPageProps
|
export interface OrderListPageProps
|
||||||
extends PageListProps,
|
extends PageListProps,
|
||||||
ListActions,
|
ListActions,
|
||||||
FilterPageProps<OrderFilterKeys>,
|
FilterPageProps<OrderFilterKeys, OrderListFilterOpts>,
|
||||||
SortPage<OrderListUrlSortField> {
|
SortPage<OrderListUrlSortField> {
|
||||||
orders: OrderList_orders_edges_node[];
|
orders: OrderList_orders_edges_node[];
|
||||||
}
|
}
|
||||||
|
@ -29,13 +34,13 @@ export interface OrderListPageProps
|
||||||
const OrderListPage: React.FC<OrderListPageProps> = ({
|
const OrderListPage: React.FC<OrderListPageProps> = ({
|
||||||
currencySymbol,
|
currencySymbol,
|
||||||
currentTab,
|
currentTab,
|
||||||
filtersList,
|
|
||||||
initialSearch,
|
initialSearch,
|
||||||
|
filterOpts,
|
||||||
tabs,
|
tabs,
|
||||||
onAdd,
|
onAdd,
|
||||||
onAll,
|
onAll,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onFilterAdd,
|
onFilterChange,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
onTabDelete,
|
onTabDelete,
|
||||||
onTabSave,
|
onTabSave,
|
||||||
|
@ -43,6 +48,8 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const filterStructure = createFilterStructure(intl, filterOpts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<PageHeader title={intl.formatMessage(sectionNames.orders)}>
|
<PageHeader title={intl.formatMessage(sectionNames.orders)}>
|
||||||
|
@ -54,28 +61,25 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Card>
|
<Card>
|
||||||
<OrderListFilter
|
<FilterBar
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
|
currentTab={currentTab}
|
||||||
|
initialSearch={initialSearch}
|
||||||
|
onAll={onAll}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
|
onSearchChange={onSearchChange}
|
||||||
|
onTabChange={onTabChange}
|
||||||
|
onTabDelete={onTabDelete}
|
||||||
|
onTabSave={onTabSave}
|
||||||
|
tabs={tabs}
|
||||||
allTabLabel={intl.formatMessage({
|
allTabLabel={intl.formatMessage({
|
||||||
defaultMessage: "All Orders",
|
defaultMessage: "All Orders",
|
||||||
description: "tab name"
|
description: "tab name"
|
||||||
})}
|
})}
|
||||||
currencySymbol={currencySymbol}
|
filterStructure={filterStructure}
|
||||||
currentTab={currentTab}
|
|
||||||
filterLabel={intl.formatMessage({
|
|
||||||
defaultMessage: "Select all orders where:"
|
|
||||||
})}
|
|
||||||
tabs={tabs}
|
|
||||||
filtersList={filtersList}
|
|
||||||
initialSearch={initialSearch}
|
|
||||||
searchPlaceholder={intl.formatMessage({
|
searchPlaceholder={intl.formatMessage({
|
||||||
defaultMessage: "Search Orders..."
|
defaultMessage: "Search Orders..."
|
||||||
})}
|
})}
|
||||||
onAll={onAll}
|
|
||||||
onSearchChange={onSearchChange}
|
|
||||||
onFilterAdd={onFilterAdd}
|
|
||||||
onTabChange={onTabChange}
|
|
||||||
onTabDelete={onTabDelete}
|
|
||||||
onTabSave={onTabSave}
|
|
||||||
/>
|
/>
|
||||||
<OrderList {...listProps} />
|
<OrderList {...listProps} />
|
||||||
</Card>
|
</Card>
|
||||||
|
|
71
src/orders/components/OrderListPage/filters.ts
Normal file
71
src/orders/components/OrderListPage/filters.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { defineMessages, IntlShape } from "react-intl";
|
||||||
|
|
||||||
|
import { FilterOpts, MinMax } from "@saleor/types";
|
||||||
|
import { OrderStatusFilter } from "@saleor/types/globalTypes";
|
||||||
|
import {
|
||||||
|
createDateField,
|
||||||
|
createOptionsField
|
||||||
|
} from "@saleor/utils/filters/fields";
|
||||||
|
import { IFilter } from "@saleor/components/Filter";
|
||||||
|
import { orderStatusMessages } from "@saleor/misc";
|
||||||
|
import { commonMessages } from "@saleor/intl";
|
||||||
|
|
||||||
|
export enum OrderFilterKeys {
|
||||||
|
created = "created",
|
||||||
|
status = "status"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderListFilterOpts {
|
||||||
|
created: FilterOpts<MinMax>;
|
||||||
|
status: FilterOpts<OrderStatusFilter[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
placed: {
|
||||||
|
defaultMessage: "Placed",
|
||||||
|
description: "order"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createFilterStructure(
|
||||||
|
intl: IntlShape,
|
||||||
|
opts: OrderListFilterOpts
|
||||||
|
): IFilter<OrderFilterKeys> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...createDateField(
|
||||||
|
OrderFilterKeys.created,
|
||||||
|
intl.formatMessage(messages.placed),
|
||||||
|
opts.created.value
|
||||||
|
),
|
||||||
|
active: opts.created.active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...createOptionsField(
|
||||||
|
OrderFilterKeys.status,
|
||||||
|
intl.formatMessage(commonMessages.status),
|
||||||
|
opts.status.value,
|
||||||
|
true,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(orderStatusMessages.cancelled),
|
||||||
|
value: OrderStatusFilter.CANCELED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(orderStatusMessages.fulfilled),
|
||||||
|
value: OrderStatusFilter.FULFILLED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(orderStatusMessages.partiallyFulfilled),
|
||||||
|
value: OrderStatusFilter.PARTIALLY_FULFILLED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(orderStatusMessages.unfulfilled),
|
||||||
|
value: OrderStatusFilter.UNFULFILLED
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
active: opts.status.active
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
export { default } from "./OrderListPage";
|
export { default } from "./OrderListPage";
|
||||||
export * from "./OrderListPage";
|
export * from "./OrderListPage";
|
||||||
|
export * from "./filters";
|
||||||
|
|
|
@ -17,8 +17,8 @@ const orderSectionUrl = "/orders";
|
||||||
|
|
||||||
export const orderListPath = orderSectionUrl;
|
export const orderListPath = orderSectionUrl;
|
||||||
export enum OrderListUrlFiltersEnum {
|
export enum OrderListUrlFiltersEnum {
|
||||||
dateFrom = "dateFrom",
|
createdFrom = "createdFrom",
|
||||||
dateTo = "dateTo",
|
createdTo = "createdTo",
|
||||||
email = "email",
|
email = "email",
|
||||||
payment = "payment",
|
payment = "payment",
|
||||||
query = "query"
|
query = "query"
|
||||||
|
@ -55,6 +55,9 @@ export const orderListUrl = (params?: OrderListUrlQueryParams): string => {
|
||||||
|
|
||||||
export const orderDraftListPath = urlJoin(orderSectionUrl, "drafts");
|
export const orderDraftListPath = urlJoin(orderSectionUrl, "drafts");
|
||||||
export enum OrderDraftListUrlFiltersEnum {
|
export enum OrderDraftListUrlFiltersEnum {
|
||||||
|
createdFrom = "createdFrom",
|
||||||
|
createdTo = "createdTo",
|
||||||
|
customer = "customer",
|
||||||
query = "query"
|
query = "query"
|
||||||
}
|
}
|
||||||
export type OrderDraftListUrlFilters = Filters<OrderDraftListUrlFiltersEnum>;
|
export type OrderDraftListUrlFilters = Filters<OrderDraftListUrlFiltersEnum>;
|
||||||
|
|
|
@ -21,6 +21,8 @@ import { ListViews } from "@saleor/types";
|
||||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||||
import { getSortParams } from "@saleor/utils/sort";
|
import { getSortParams } from "@saleor/utils/sort";
|
||||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||||
|
import useShop from "@saleor/hooks/useShop";
|
||||||
|
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||||
import OrderDraftListPage from "../../components/OrderDraftListPage";
|
import OrderDraftListPage from "../../components/OrderDraftListPage";
|
||||||
import {
|
import {
|
||||||
TypedOrderDraftBulkCancelMutation,
|
TypedOrderDraftBulkCancelMutation,
|
||||||
|
@ -31,7 +33,6 @@ import { OrderDraftBulkCancel } from "../../types/OrderDraftBulkCancel";
|
||||||
import { OrderDraftCreate } from "../../types/OrderDraftCreate";
|
import { OrderDraftCreate } from "../../types/OrderDraftCreate";
|
||||||
import {
|
import {
|
||||||
orderDraftListUrl,
|
orderDraftListUrl,
|
||||||
OrderDraftListUrlFilters,
|
|
||||||
OrderDraftListUrlQueryParams,
|
OrderDraftListUrlQueryParams,
|
||||||
orderUrl,
|
orderUrl,
|
||||||
OrderDraftListUrlDialog
|
OrderDraftListUrlDialog
|
||||||
|
@ -42,8 +43,10 @@ import {
|
||||||
getActiveFilters,
|
getActiveFilters,
|
||||||
getFilterTabs,
|
getFilterTabs,
|
||||||
getFilterVariables,
|
getFilterVariables,
|
||||||
saveFilterTab
|
saveFilterTab,
|
||||||
} from "./filter";
|
getFilterQueryParam,
|
||||||
|
getFilterOpts
|
||||||
|
} from "./filters";
|
||||||
import { getSortQueryVariables } from "./sort";
|
import { getSortQueryVariables } from "./sort";
|
||||||
|
|
||||||
interface OrderDraftListProps {
|
interface OrderDraftListProps {
|
||||||
|
@ -61,6 +64,7 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
|
||||||
ListViews.DRAFT_LIST
|
ListViews.DRAFT_LIST
|
||||||
);
|
);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const shop = useShop();
|
||||||
|
|
||||||
const handleCreateOrderCreateSuccess = (data: OrderDraftCreate) => {
|
const handleCreateOrderCreateSuccess = (data: OrderDraftCreate) => {
|
||||||
notify({
|
notify({
|
||||||
|
@ -84,16 +88,17 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
|
||||||
: 0
|
: 0
|
||||||
: parseInt(params.activeTab, 0);
|
: parseInt(params.activeTab, 0);
|
||||||
|
|
||||||
const changeFilterField = (filter: OrderDraftListUrlFilters) => {
|
const [
|
||||||
reset();
|
changeFilters,
|
||||||
navigate(
|
resetFilters,
|
||||||
orderDraftListUrl({
|
handleSearchChange
|
||||||
...getActiveFilters(params),
|
] = createFilterHandlers({
|
||||||
...filter,
|
cleanupFn: reset,
|
||||||
activeTab: undefined
|
createUrl: orderDraftListUrl,
|
||||||
})
|
getFilterQueryParam,
|
||||||
);
|
navigate,
|
||||||
};
|
params
|
||||||
|
});
|
||||||
|
|
||||||
const [openModal, closeModal] = createDialogActionHandlers<
|
const [openModal, closeModal] = createDialogActionHandlers<
|
||||||
OrderDraftListUrlDialog,
|
OrderDraftListUrlDialog,
|
||||||
|
@ -155,6 +160,7 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSort = createSortHandler(navigate, orderDraftListUrl, params);
|
const handleSort = createSortHandler(navigate, orderDraftListUrl, params);
|
||||||
|
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TypedOrderDraftBulkCancelMutation onCompleted={handleOrderDraftBulkCancel}>
|
<TypedOrderDraftBulkCancelMutation onCompleted={handleOrderDraftBulkCancel}>
|
||||||
|
@ -169,10 +175,13 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OrderDraftListPage
|
<OrderDraftListPage
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
filterOpts={getFilterOpts(params)}
|
||||||
initialSearch={params.query || ""}
|
initialSearch={params.query || ""}
|
||||||
onSearchChange={query => changeFilterField({ query })}
|
onSearchChange={handleSearchChange}
|
||||||
onAll={() => navigate(orderDraftListUrl())}
|
onFilterChange={changeFilters}
|
||||||
|
onAll={resetFilters}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
onTabDelete={() => openModal("delete-search")}
|
onTabDelete={() => openModal("delete-search")}
|
||||||
onTabSave={() => openModal("save-search")}
|
onTabSave={() => openModal("save-search")}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
|
||||||
|
Object {
|
||||||
|
"createdFrom": "2019-12-09",
|
||||||
|
"createdTo": "2019-12-38",
|
||||||
|
"customer": "admin@example.com",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"createdFrom=2019-12-09&createdTo=2019-12-38&customer=admin%40example.com"`;
|
|
@ -1,31 +0,0 @@
|
||||||
import { OrderDraftFilterInput } from "@saleor/types/globalTypes";
|
|
||||||
import {
|
|
||||||
createFilterTabUtils,
|
|
||||||
createFilterUtils
|
|
||||||
} from "../../../utils/filters";
|
|
||||||
import {
|
|
||||||
OrderDraftListUrlFilters,
|
|
||||||
OrderDraftListUrlFiltersEnum,
|
|
||||||
OrderDraftListUrlQueryParams
|
|
||||||
} from "../../urls";
|
|
||||||
|
|
||||||
export const ORDER_DRAFT_FILTERS_KEY = "orderDraftFilters";
|
|
||||||
|
|
||||||
export function getFilterVariables(
|
|
||||||
params: OrderDraftListUrlFilters
|
|
||||||
): OrderDraftFilterInput {
|
|
||||||
return {
|
|
||||||
search: params.query
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const {
|
|
||||||
deleteFilterTab,
|
|
||||||
getFilterTabs,
|
|
||||||
saveFilterTab
|
|
||||||
} = createFilterTabUtils<OrderDraftListUrlFilters>(ORDER_DRAFT_FILTERS_KEY);
|
|
||||||
|
|
||||||
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
|
|
||||||
OrderDraftListUrlQueryParams,
|
|
||||||
OrderDraftListUrlFilters
|
|
||||||
>(OrderDraftListUrlFiltersEnum);
|
|
67
src/orders/views/OrderDraftList/filters.test.ts
Normal file
67
src/orders/views/OrderDraftList/filters.test.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { createIntl } from "react-intl";
|
||||||
|
import { stringify as stringifyQs } from "qs";
|
||||||
|
|
||||||
|
import { OrderDraftListUrlFilters } from "@saleor/orders/urls";
|
||||||
|
import { createFilterStructure } from "@saleor/orders/components/OrderDraftListPage";
|
||||||
|
import { getFilterQueryParams } from "@saleor/utils/filters";
|
||||||
|
import { date } from "@saleor/fixtures";
|
||||||
|
import { getExistingKeys, setFilterOptsStatus } from "@test/filters";
|
||||||
|
import { config } from "@test/intl";
|
||||||
|
import { getFilterVariables, getFilterQueryParam } from "./filters";
|
||||||
|
|
||||||
|
describe("Filtering query params", () => {
|
||||||
|
it("should be empty object if no params given", () => {
|
||||||
|
const params: OrderDraftListUrlFilters = {};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterVariables)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be empty object if params given", () => {
|
||||||
|
const params: OrderDraftListUrlFilters = {
|
||||||
|
createdFrom: date.from,
|
||||||
|
createdTo: date.to,
|
||||||
|
customer: "admin@example.com"
|
||||||
|
};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterVariables)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Filtering URL params", () => {
|
||||||
|
const intl = createIntl(config);
|
||||||
|
|
||||||
|
const filters = createFilterStructure(intl, {
|
||||||
|
created: {
|
||||||
|
active: false,
|
||||||
|
value: {
|
||||||
|
max: date.to,
|
||||||
|
min: date.from
|
||||||
|
}
|
||||||
|
},
|
||||||
|
customer: {
|
||||||
|
active: false,
|
||||||
|
value: "admin@example.com"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be empty if no active filters", () => {
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
filters,
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterQueryParams)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be empty if active filters are present", () => {
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
setFilterOptsStatus(filters, true),
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(filterQueryParams).toMatchSnapshot();
|
||||||
|
expect(stringifyQs(filterQueryParams)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
90
src/orders/views/OrderDraftList/filters.ts
Normal file
90
src/orders/views/OrderDraftList/filters.ts
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import { OrderDraftFilterInput } from "@saleor/types/globalTypes";
|
||||||
|
import { maybe } from "@saleor/misc";
|
||||||
|
import { IFilterElement } from "@saleor/components/Filter";
|
||||||
|
import {
|
||||||
|
OrderDraftFilterKeys,
|
||||||
|
OrderDraftListFilterOpts
|
||||||
|
} from "@saleor/orders/components/OrderDraftListPage";
|
||||||
|
import {
|
||||||
|
OrderDraftListUrlFilters,
|
||||||
|
OrderDraftListUrlFiltersEnum,
|
||||||
|
OrderDraftListUrlQueryParams
|
||||||
|
} from "../../urls";
|
||||||
|
import {
|
||||||
|
createFilterTabUtils,
|
||||||
|
createFilterUtils,
|
||||||
|
getGteLteVariables,
|
||||||
|
getMinMaxQueryParam,
|
||||||
|
getSingleValueQueryParam
|
||||||
|
} from "../../../utils/filters";
|
||||||
|
|
||||||
|
export const ORDER_DRAFT_FILTERS_KEY = "orderDraftFilters";
|
||||||
|
|
||||||
|
export function getFilterOpts(
|
||||||
|
params: OrderDraftListUrlFilters
|
||||||
|
): OrderDraftListFilterOpts {
|
||||||
|
return {
|
||||||
|
created: {
|
||||||
|
active: maybe(
|
||||||
|
() =>
|
||||||
|
[params.createdFrom, params.createdTo].some(
|
||||||
|
field => field !== undefined
|
||||||
|
),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
max: maybe(() => params.createdTo),
|
||||||
|
min: maybe(() => params.createdFrom)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
customer: {
|
||||||
|
active: !!maybe(() => params.customer),
|
||||||
|
value: params.customer
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilterVariables(
|
||||||
|
params: OrderDraftListUrlFilters
|
||||||
|
): OrderDraftFilterInput {
|
||||||
|
return {
|
||||||
|
created: getGteLteVariables({
|
||||||
|
gte: params.createdFrom,
|
||||||
|
lte: params.createdTo
|
||||||
|
}),
|
||||||
|
customer: params.customer,
|
||||||
|
search: params.query
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilterQueryParam(
|
||||||
|
filter: IFilterElement<OrderDraftFilterKeys>
|
||||||
|
): OrderDraftListUrlFilters {
|
||||||
|
const { name } = filter;
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case OrderDraftFilterKeys.created:
|
||||||
|
return getMinMaxQueryParam(
|
||||||
|
filter,
|
||||||
|
OrderDraftListUrlFiltersEnum.createdFrom,
|
||||||
|
OrderDraftListUrlFiltersEnum.createdTo
|
||||||
|
);
|
||||||
|
|
||||||
|
case OrderDraftFilterKeys.customer:
|
||||||
|
return getSingleValueQueryParam(
|
||||||
|
filter,
|
||||||
|
OrderDraftListUrlFiltersEnum.customer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const {
|
||||||
|
deleteFilterTab,
|
||||||
|
getFilterTabs,
|
||||||
|
saveFilterTab
|
||||||
|
} = createFilterTabUtils<OrderDraftListUrlFilters>(ORDER_DRAFT_FILTERS_KEY);
|
||||||
|
|
||||||
|
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
|
||||||
|
OrderDraftListUrlQueryParams,
|
||||||
|
OrderDraftListUrlFilters
|
||||||
|
>(OrderDraftListUrlFiltersEnum);
|
|
@ -7,7 +7,6 @@ import SaveFilterTabDialog, {
|
||||||
SaveFilterTabDialogFormData
|
SaveFilterTabDialogFormData
|
||||||
} from "@saleor/components/SaveFilterTabDialog";
|
} from "@saleor/components/SaveFilterTabDialog";
|
||||||
import useBulkActions from "@saleor/hooks/useBulkActions";
|
import useBulkActions from "@saleor/hooks/useBulkActions";
|
||||||
import useDateLocalize from "@saleor/hooks/useDateLocalize";
|
|
||||||
import useListSettings from "@saleor/hooks/useListSettings";
|
import useListSettings from "@saleor/hooks/useListSettings";
|
||||||
import useNavigator from "@saleor/hooks/useNavigator";
|
import useNavigator from "@saleor/hooks/useNavigator";
|
||||||
import useNotifier from "@saleor/hooks/useNotifier";
|
import useNotifier from "@saleor/hooks/useNotifier";
|
||||||
|
@ -20,6 +19,7 @@ import { ListViews } from "@saleor/types";
|
||||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||||
import { getSortParams } from "@saleor/utils/sort";
|
import { getSortParams } from "@saleor/utils/sort";
|
||||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||||
|
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||||
import OrderBulkCancelDialog from "../../components/OrderBulkCancelDialog";
|
import OrderBulkCancelDialog from "../../components/OrderBulkCancelDialog";
|
||||||
import OrderListPage from "../../components/OrderListPage/OrderListPage";
|
import OrderListPage from "../../components/OrderListPage/OrderListPage";
|
||||||
import {
|
import {
|
||||||
|
@ -31,20 +31,19 @@ import { OrderBulkCancel } from "../../types/OrderBulkCancel";
|
||||||
import { OrderDraftCreate } from "../../types/OrderDraftCreate";
|
import { OrderDraftCreate } from "../../types/OrderDraftCreate";
|
||||||
import {
|
import {
|
||||||
orderListUrl,
|
orderListUrl,
|
||||||
OrderListUrlFilters,
|
|
||||||
OrderListUrlQueryParams,
|
OrderListUrlQueryParams,
|
||||||
orderUrl,
|
orderUrl,
|
||||||
OrderListUrlDialog
|
OrderListUrlDialog
|
||||||
} from "../../urls";
|
} from "../../urls";
|
||||||
import {
|
import {
|
||||||
areFiltersApplied,
|
areFiltersApplied,
|
||||||
createFilter,
|
|
||||||
createFilterChips,
|
|
||||||
deleteFilterTab,
|
deleteFilterTab,
|
||||||
getActiveFilters,
|
getActiveFilters,
|
||||||
getFilterTabs,
|
getFilterTabs,
|
||||||
|
getFilterOpts,
|
||||||
getFilterVariables,
|
getFilterVariables,
|
||||||
saveFilterTab
|
saveFilterTab,
|
||||||
|
getFilterQueryParam
|
||||||
} from "./filters";
|
} from "./filters";
|
||||||
import { getSortQueryVariables } from "./sort";
|
import { getSortQueryVariables } from "./sort";
|
||||||
|
|
||||||
|
@ -53,7 +52,6 @@ interface OrderListProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OrderList: React.FC<OrderListProps> = ({ params }) => {
|
export const OrderList: React.FC<OrderListProps> = ({ params }) => {
|
||||||
const formatDate = useDateLocalize();
|
|
||||||
const navigate = useNavigator();
|
const navigate = useNavigator();
|
||||||
const notify = useNotifier();
|
const notify = useNotifier();
|
||||||
const paginate = usePaginator();
|
const paginate = usePaginator();
|
||||||
|
@ -88,21 +86,17 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
|
||||||
: 0
|
: 0
|
||||||
: parseInt(params.activeTab, 0);
|
: parseInt(params.activeTab, 0);
|
||||||
|
|
||||||
const changeFilters = (filters: OrderListUrlFilters) => {
|
const [
|
||||||
reset();
|
changeFilters,
|
||||||
navigate(orderListUrl(filters));
|
resetFilters,
|
||||||
};
|
handleSearchChange
|
||||||
|
] = createFilterHandlers({
|
||||||
const changeFilterField = (filter: OrderListUrlFilters) => {
|
cleanupFn: reset,
|
||||||
reset();
|
createUrl: orderListUrl,
|
||||||
navigate(
|
getFilterQueryParam,
|
||||||
orderListUrl({
|
navigate,
|
||||||
...getActiveFilters(params),
|
params
|
||||||
...filter,
|
});
|
||||||
activeTab: undefined
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [openModal, closeModal] = createDialogActionHandlers<
|
const [openModal, closeModal] = createDialogActionHandlers<
|
||||||
OrderListUrlDialog,
|
OrderListUrlDialog,
|
||||||
|
@ -183,16 +177,9 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
|
||||||
<OrderListPage
|
<OrderListPage
|
||||||
currencySymbol={currencySymbol}
|
currencySymbol={currencySymbol}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
filtersList={createFilterChips(
|
|
||||||
params,
|
|
||||||
{
|
|
||||||
formatDate
|
|
||||||
},
|
|
||||||
changeFilterField,
|
|
||||||
intl
|
|
||||||
)}
|
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
filterOpts={getFilterOpts(params)}
|
||||||
orders={maybe(() => data.orders.edges.map(edge => edge.node))}
|
orders={maybe(() => data.orders.edges.map(edge => edge.node))}
|
||||||
pageInfo={pageInfo}
|
pageInfo={pageInfo}
|
||||||
sort={getSortParams(params)}
|
sort={getSortParams(params)}
|
||||||
|
@ -221,20 +208,14 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
onSearchChange={query => changeFilterField({ query })}
|
onSearchChange={handleSearchChange}
|
||||||
onFilterAdd={data =>
|
onFilterChange={changeFilters}
|
||||||
changeFilterField(createFilter(params, data))
|
|
||||||
}
|
|
||||||
onTabSave={() => openModal("save-search")}
|
onTabSave={() => openModal("save-search")}
|
||||||
onTabDelete={() => openModal("delete-search")}
|
onTabDelete={() => openModal("delete-search")}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
initialSearch={params.query || ""}
|
initialSearch={params.query || ""}
|
||||||
tabs={getFilterTabs().map(tab => tab.name)}
|
tabs={getFilterTabs().map(tab => tab.name)}
|
||||||
onAll={() =>
|
onAll={resetFilters}
|
||||||
changeFilters({
|
|
||||||
status: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<OrderBulkCancelDialog
|
<OrderBulkCancelDialog
|
||||||
confirmButtonState={orderBulkCancelOpts.status}
|
confirmButtonState={orderBulkCancelOpts.status}
|
||||||
|
|
|
@ -1,83 +1,9 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Crate filter chips 1`] = `
|
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"label": "Date from 2019-09-01",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "Date to 2019-09-10",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "email@example.com",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "Fulfilled",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"label": "Partially Fulfilled",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Create filter object with date 1`] = `
|
|
||||||
Object {
|
|
||||||
"dateFrom": "2019-09-01",
|
|
||||||
"dateTo": "2019-09-01",
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Create filter object with date last month 1`] = `
|
|
||||||
Object {
|
|
||||||
"dateFrom": "2019-09-01",
|
|
||||||
"dateTo": undefined,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Create filter object with date last week 1`] = `
|
|
||||||
Object {
|
|
||||||
"dateFrom": "2019-09-01",
|
|
||||||
"dateTo": undefined,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Create filter object with date last year 1`] = `
|
|
||||||
Object {
|
|
||||||
"dateFrom": "2019-09-01",
|
|
||||||
"dateTo": undefined,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Create filter object with date range 1`] = `
|
|
||||||
Object {
|
|
||||||
"dateFrom": "2019-09-01",
|
|
||||||
"dateTo": "2019-09-10",
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Create filter object with fulfillment status 1`] = `
|
|
||||||
Object {
|
|
||||||
"status": Array [
|
|
||||||
"PARTIALLY_FULFILLED",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Create filter object with multiple deduped values 1`] = `
|
|
||||||
Object {
|
|
||||||
"status": Array [
|
|
||||||
"FULFILLED",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Create filter object with multiple values 1`] = `
|
|
||||||
Object {
|
Object {
|
||||||
|
"createdFrom": "2019-12-09",
|
||||||
|
"createdTo": "2019-12-38",
|
||||||
"status": Array [
|
"status": Array [
|
||||||
"FULFILLED",
|
"FULFILLED",
|
||||||
"PARTIALLY_FULFILLED",
|
"PARTIALLY_FULFILLED",
|
||||||
|
@ -85,31 +11,4 @@ Object {
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Get filter variables from multiple status value 1`] = `
|
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"createdFrom=2019-12-09&createdTo=2019-12-38&status%5B0%5D=FULFILLED&status%5B1%5D=PARTIALLY_FULFILLED"`;
|
||||||
Object {
|
|
||||||
"created": Object {
|
|
||||||
"gte": "2019-09-01",
|
|
||||||
"lte": "2019-09-10",
|
|
||||||
},
|
|
||||||
"customer": "email@example.com",
|
|
||||||
"search": "24",
|
|
||||||
"status": Array [
|
|
||||||
"FULFILLED",
|
|
||||||
"PARTIALLY_FULFILLED",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Get filter variables from single status value 1`] = `
|
|
||||||
Object {
|
|
||||||
"created": Object {
|
|
||||||
"gte": "2019-09-01",
|
|
||||||
"lte": "2019-09-10",
|
|
||||||
},
|
|
||||||
"customer": "email@example.com",
|
|
||||||
"search": "24",
|
|
||||||
"status": Array [
|
|
||||||
"FULFILLED",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
|
@ -1,158 +1,75 @@
|
||||||
import { createIntl } from "react-intl";
|
import { createIntl } from "react-intl";
|
||||||
|
import { stringify as stringifyQs } from "qs";
|
||||||
|
|
||||||
import { OrderFilterKeys } from "@saleor/orders/components/OrderListFilter";
|
import { OrderListUrlFilters } from "@saleor/orders/urls";
|
||||||
import { OrderStatus, OrderStatusFilter } from "@saleor/types/globalTypes";
|
import { createFilterStructure } from "@saleor/orders/components/OrderListPage";
|
||||||
import { createFilter, createFilterChips, getFilterVariables } from "./filters";
|
import { getFilterQueryParams } from "@saleor/utils/filters";
|
||||||
|
import { date } from "@saleor/fixtures";
|
||||||
|
import { getExistingKeys, setFilterOptsStatus } from "@test/filters";
|
||||||
|
import { config } from "@test/intl";
|
||||||
|
import { OrderStatusFilter } from "@saleor/types/globalTypes";
|
||||||
|
import { getFilterVariables, getFilterQueryParam } from "./filters";
|
||||||
|
|
||||||
const mockIntl = createIntl({
|
describe("Filtering query params", () => {
|
||||||
locale: "en"
|
it("should be empty object if no params given", () => {
|
||||||
});
|
const params: OrderListUrlFilters = {};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
describe("Create filter object", () => {
|
expect(getExistingKeys(filterVariables)).toHaveLength(0);
|
||||||
it("with date", () => {
|
|
||||||
const filter = createFilter(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
name: OrderFilterKeys.dateEqual,
|
|
||||||
value: "2019-09-01"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(filter).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("with date range", () => {
|
it("should not be empty object if params given", () => {
|
||||||
const filter = createFilter(
|
const params: OrderListUrlFilters = {
|
||||||
{},
|
createdFrom: date.from,
|
||||||
{
|
createdTo: date.to,
|
||||||
name: OrderFilterKeys.dateRange,
|
|
||||||
value: ["2019-09-01", "2019-09-10"]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(filter).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("with date last week", () => {
|
|
||||||
const filter = createFilter(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
name: OrderFilterKeys.dateLastWeek,
|
|
||||||
value: "2019-09-01"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(filter).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("with date last month", () => {
|
|
||||||
const filter = createFilter(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
name: OrderFilterKeys.dateLastMonth,
|
|
||||||
value: "2019-09-01"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(filter).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("with date last year", () => {
|
|
||||||
const filter = createFilter(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
name: OrderFilterKeys.dateLastYear,
|
|
||||||
value: "2019-09-01"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(filter).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("with fulfillment status", () => {
|
|
||||||
const filter = createFilter(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
name: OrderFilterKeys.status,
|
|
||||||
value: OrderStatusFilter.PARTIALLY_FULFILLED
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(filter).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("with multiple values", () => {
|
|
||||||
const filter = createFilter(
|
|
||||||
{
|
|
||||||
status: [OrderStatusFilter.FULFILLED]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: OrderFilterKeys.status,
|
|
||||||
value: OrderStatusFilter.PARTIALLY_FULFILLED
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(filter).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("with multiple deduped values", () => {
|
|
||||||
const filter = createFilter(
|
|
||||||
{
|
|
||||||
status: [OrderStatusFilter.FULFILLED]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: OrderFilterKeys.status,
|
|
||||||
value: OrderStatusFilter.FULFILLED
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(filter).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Crate filter chips", () => {
|
|
||||||
const chips = createFilterChips(
|
|
||||||
{
|
|
||||||
dateFrom: "2019-09-01",
|
|
||||||
dateTo: "2019-09-10",
|
|
||||||
email: "email@example.com",
|
email: "email@example.com",
|
||||||
status: [OrderStatus.FULFILLED, OrderStatus.PARTIALLY_FULFILLED]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
formatDate: date => date
|
|
||||||
},
|
|
||||||
jest.fn(),
|
|
||||||
mockIntl as any
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(chips).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Get filter variables", () => {
|
|
||||||
it("from single status value", () => {
|
|
||||||
const filter = getFilterVariables({
|
|
||||||
dateFrom: "2019-09-01",
|
|
||||||
dateTo: "2019-09-10",
|
|
||||||
email: "email@example.com",
|
|
||||||
query: "24",
|
|
||||||
status: OrderStatus.FULFILLED.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(filter).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("from multiple status value", () => {
|
|
||||||
const filter = getFilterVariables({
|
|
||||||
dateFrom: "2019-09-01",
|
|
||||||
dateTo: "2019-09-10",
|
|
||||||
email: "email@example.com",
|
|
||||||
query: "24",
|
|
||||||
status: [
|
status: [
|
||||||
OrderStatus.FULFILLED.toString(),
|
OrderStatusFilter.FULFILLED,
|
||||||
OrderStatus.PARTIALLY_FULFILLED.toString()
|
OrderStatusFilter.PARTIALLY_FULFILLED
|
||||||
]
|
]
|
||||||
});
|
};
|
||||||
|
const filterVariables = getFilterVariables(params);
|
||||||
|
|
||||||
expect(filter).toMatchSnapshot();
|
expect(getExistingKeys(filterVariables)).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Filtering URL params", () => {
|
||||||
|
const intl = createIntl(config);
|
||||||
|
|
||||||
|
const filters = createFilterStructure(intl, {
|
||||||
|
created: {
|
||||||
|
active: false,
|
||||||
|
value: {
|
||||||
|
max: date.to,
|
||||||
|
min: date.from
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
active: false,
|
||||||
|
value: [
|
||||||
|
OrderStatusFilter.FULFILLED,
|
||||||
|
OrderStatusFilter.PARTIALLY_FULFILLED
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be empty if no active filters", () => {
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
filters,
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getExistingKeys(filterQueryParams)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be empty if active filters are present", () => {
|
||||||
|
const filterQueryParams = getFilterQueryParams(
|
||||||
|
setFilterOptsStatus(filters, true),
|
||||||
|
getFilterQueryParam
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(filterQueryParams).toMatchSnapshot();
|
||||||
|
expect(stringifyQs(filterQueryParams)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import { defineMessages, IntlShape } from "react-intl";
|
import { findInEnum, maybe, findValueInEnum } from "@saleor/misc";
|
||||||
|
import {
|
||||||
import { findInEnum } from "@saleor/misc";
|
OrderListFilterOpts,
|
||||||
import { removeAtIndex } from "@saleor/utils/lists";
|
OrderFilterKeys
|
||||||
import { FilterContentSubmitData } from "../../../components/Filter";
|
} from "@saleor/orders/components/OrderListPage/filters";
|
||||||
import { Filter } from "../../../components/TableFilter";
|
import { IFilterElement } from "../../../components/Filter";
|
||||||
import {
|
import {
|
||||||
OrderFilterInput,
|
OrderFilterInput,
|
||||||
OrderStatusFilter
|
OrderStatusFilter,
|
||||||
|
OrderStatus
|
||||||
} from "../../../types/globalTypes";
|
} from "../../../types/globalTypes";
|
||||||
import {
|
import {
|
||||||
arrayOrUndefined,
|
|
||||||
arrayOrValue,
|
|
||||||
createFilterTabUtils,
|
createFilterTabUtils,
|
||||||
createFilterUtils,
|
createFilterUtils,
|
||||||
dedupeFilter,
|
dedupeFilter,
|
||||||
valueOrFirst
|
getGteLteVariables,
|
||||||
|
getMinMaxQueryParam,
|
||||||
|
getMultipleEnumValueQueryParam
|
||||||
} from "../../../utils/filters";
|
} from "../../../utils/filters";
|
||||||
import { OrderFilterKeys } from "../../components/OrderListFilter";
|
|
||||||
import {
|
import {
|
||||||
OrderListUrlFilters,
|
OrderListUrlFilters,
|
||||||
OrderListUrlFiltersEnum,
|
OrderListUrlFiltersEnum,
|
||||||
|
@ -26,215 +26,76 @@ import {
|
||||||
|
|
||||||
export const ORDER_FILTERS_KEY = "orderFilters";
|
export const ORDER_FILTERS_KEY = "orderFilters";
|
||||||
|
|
||||||
const filterMessages = defineMessages({
|
export function getFilterOpts(
|
||||||
dateFrom: {
|
params: OrderListUrlFilters
|
||||||
defaultMessage: "Date from {date}",
|
): OrderListFilterOpts {
|
||||||
description: "filter by date"
|
return {
|
||||||
},
|
created: {
|
||||||
dateIs: {
|
active: maybe(
|
||||||
defaultMessage: "Date is {date}",
|
() =>
|
||||||
description: "filter by date"
|
[params.createdFrom, params.createdTo].some(
|
||||||
},
|
field => field !== undefined
|
||||||
dateTo: {
|
),
|
||||||
defaultMessage: "Date to {date}",
|
false
|
||||||
description: "filter by date"
|
),
|
||||||
},
|
value: {
|
||||||
fulfilled: {
|
max: maybe(() => params.createdTo, ""),
|
||||||
defaultMessage: "Fulfilled",
|
min: maybe(() => params.createdFrom, "")
|
||||||
description: "order status"
|
}
|
||||||
},
|
},
|
||||||
partiallyFulfilled: {
|
status: {
|
||||||
defaultMessage: "Partially Fulfilled",
|
active: maybe(() => params.status !== undefined, false),
|
||||||
description: "order status"
|
value: maybe(
|
||||||
},
|
() =>
|
||||||
readyToCapture: {
|
dedupeFilter(
|
||||||
defaultMessage: "Ready to Capture",
|
params.status.map(status =>
|
||||||
description: "order status"
|
findValueInEnum(status, OrderStatusFilter)
|
||||||
},
|
)
|
||||||
unfulfilled: {
|
),
|
||||||
defaultMessage: "Unfulfilled",
|
[]
|
||||||
description: "order status"
|
)
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
function getStatusLabel(status: string, intl: IntlShape): string {
|
|
||||||
switch (status) {
|
|
||||||
case OrderStatusFilter.FULFILLED.toString():
|
|
||||||
return intl.formatMessage(filterMessages.fulfilled);
|
|
||||||
|
|
||||||
case OrderStatusFilter.PARTIALLY_FULFILLED.toString():
|
|
||||||
return intl.formatMessage(filterMessages.partiallyFulfilled);
|
|
||||||
|
|
||||||
case OrderStatusFilter.UNFULFILLED.toString():
|
|
||||||
return intl.formatMessage(filterMessages.unfulfilled);
|
|
||||||
|
|
||||||
case OrderStatusFilter.READY_TO_CAPTURE.toString():
|
|
||||||
return intl.formatMessage(filterMessages.readyToCapture);
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFilterVariables(
|
export function getFilterVariables(
|
||||||
params: OrderListUrlFilters
|
params: OrderListUrlFilters
|
||||||
): OrderFilterInput {
|
): OrderFilterInput {
|
||||||
return {
|
return {
|
||||||
created: {
|
created: getGteLteVariables({
|
||||||
gte: params.dateFrom,
|
gte: params.createdFrom,
|
||||||
lte: params.dateTo
|
lte: params.createdTo
|
||||||
},
|
}),
|
||||||
customer: params.email,
|
customer: params.email,
|
||||||
search: params.query,
|
search: params.query,
|
||||||
status: Array.isArray(params.status)
|
status: maybe(() =>
|
||||||
? params.status.map(status => findInEnum(status, OrderStatusFilter))
|
params.status.map(status => findInEnum(status, OrderStatusFilter))
|
||||||
: params.status
|
)
|
||||||
? [findInEnum(params.status, OrderStatusFilter)]
|
|
||||||
: undefined
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFilter(
|
export function getFilterQueryParam(
|
||||||
filter: OrderListUrlFilters,
|
filter: IFilterElement<OrderFilterKeys>
|
||||||
data: FilterContentSubmitData<OrderFilterKeys>
|
|
||||||
): OrderListUrlFilters {
|
): OrderListUrlFilters {
|
||||||
const { name: filterName, value } = data;
|
const { name } = filter;
|
||||||
if (filterName === OrderFilterKeys.dateEqual) {
|
|
||||||
return {
|
switch (name) {
|
||||||
dateFrom: valueOrFirst(value),
|
case OrderFilterKeys.created:
|
||||||
dateTo: valueOrFirst(value)
|
return getMinMaxQueryParam(
|
||||||
};
|
filter,
|
||||||
} else if (filterName === OrderFilterKeys.dateRange) {
|
OrderListUrlFiltersEnum.createdFrom,
|
||||||
return {
|
OrderListUrlFiltersEnum.createdTo
|
||||||
dateFrom: value[0],
|
);
|
||||||
dateTo: value[1]
|
|
||||||
};
|
case OrderFilterKeys.status:
|
||||||
} else if (
|
return getMultipleEnumValueQueryParam(
|
||||||
[
|
filter,
|
||||||
OrderFilterKeys.dateLastWeek,
|
OrderListUrlFiltersWithMultipleValuesEnum.status,
|
||||||
OrderFilterKeys.dateLastMonth,
|
OrderStatus
|
||||||
OrderFilterKeys.dateLastYear
|
);
|
||||||
].includes(filterName)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
dateFrom: valueOrFirst(value),
|
|
||||||
dateTo: undefined
|
|
||||||
};
|
|
||||||
} else if (filterName === OrderFilterKeys.status) {
|
|
||||||
return {
|
|
||||||
status: dedupeFilter(
|
|
||||||
filter.status
|
|
||||||
? [...(filter.status as string[]), valueOrFirst(value)]
|
|
||||||
: arrayOrValue(value)
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OrderListChipFormatData {
|
|
||||||
formatDate: (date: string) => string;
|
|
||||||
}
|
|
||||||
export function createFilterChips(
|
|
||||||
filters: OrderListUrlFilters,
|
|
||||||
formatData: OrderListChipFormatData,
|
|
||||||
onFilterDelete: (filters: OrderListUrlFilters) => void,
|
|
||||||
intl: IntlShape
|
|
||||||
): Filter[] {
|
|
||||||
let filterChips: Filter[] = [];
|
|
||||||
|
|
||||||
if (!!filters.dateFrom || !!filters.dateTo) {
|
|
||||||
if (filters.dateFrom === filters.dateTo) {
|
|
||||||
filterChips = [
|
|
||||||
...filterChips,
|
|
||||||
{
|
|
||||||
label: intl.formatMessage(filterMessages.dateIs, {
|
|
||||||
date: formatData.formatDate(filters.dateFrom)
|
|
||||||
}),
|
|
||||||
onClick: () =>
|
|
||||||
onFilterDelete({
|
|
||||||
...filters,
|
|
||||||
dateFrom: undefined,
|
|
||||||
dateTo: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
if (!!filters.dateFrom) {
|
|
||||||
filterChips = [
|
|
||||||
...filterChips,
|
|
||||||
{
|
|
||||||
label: intl.formatMessage(filterMessages.dateFrom, {
|
|
||||||
date: formatData.formatDate(filters.dateFrom)
|
|
||||||
}),
|
|
||||||
onClick: () =>
|
|
||||||
onFilterDelete({
|
|
||||||
...filters,
|
|
||||||
dateFrom: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!!filters.dateTo) {
|
|
||||||
filterChips = [
|
|
||||||
...filterChips,
|
|
||||||
{
|
|
||||||
label: intl.formatMessage(filterMessages.dateTo, {
|
|
||||||
date: formatData.formatDate(filters.dateTo)
|
|
||||||
}),
|
|
||||||
onClick: () =>
|
|
||||||
onFilterDelete({
|
|
||||||
...filters,
|
|
||||||
dateTo: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!!filters.email) {
|
|
||||||
filterChips = [
|
|
||||||
...filterChips,
|
|
||||||
{
|
|
||||||
label: filters.email,
|
|
||||||
onClick: () =>
|
|
||||||
onFilterDelete({
|
|
||||||
...filters,
|
|
||||||
email: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!!filters.status) {
|
|
||||||
const statusFilterChips = Array.isArray(filters.status)
|
|
||||||
? filters.status.map((status, statusIndex) => ({
|
|
||||||
label: getStatusLabel(status, intl),
|
|
||||||
onClick: () =>
|
|
||||||
onFilterDelete({
|
|
||||||
...filters,
|
|
||||||
status: arrayOrUndefined(
|
|
||||||
removeAtIndex(filters.status as string[], statusIndex)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
label: getStatusLabel(filters.status, intl),
|
|
||||||
onClick: () =>
|
|
||||||
onFilterDelete({
|
|
||||||
...filters,
|
|
||||||
status: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
filterChips = [...filterChips, ...statusFilterChips];
|
|
||||||
}
|
|
||||||
|
|
||||||
return filterChips;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
deleteFilterTab,
|
deleteFilterTab,
|
||||||
getFilterTabs,
|
getFilterTabs,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import Card from "@material-ui/core/Card";
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import TableBody from "@material-ui/core/TableBody";
|
import TableBody from "@material-ui/core/TableBody";
|
||||||
import TableCell from "@material-ui/core/TableCell";
|
import TableCell from "@material-ui/core/TableCell";
|
||||||
|
@ -65,107 +64,105 @@ const PluginList: React.FC<PluginListProps> = props => {
|
||||||
onPreviousPage
|
onPreviousPage
|
||||||
} = props;
|
} = props;
|
||||||
const classes = useStyles(props);
|
const classes = useStyles(props);
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<ResponsiveTable>
|
||||||
<ResponsiveTable>
|
<TableHead>
|
||||||
<TableHead>
|
<TableCellHeader
|
||||||
<TableCellHeader
|
direction={
|
||||||
direction={
|
sort.sort === PluginListUrlSortField.name
|
||||||
sort.sort === PluginListUrlSortField.name
|
? getArrowDirection(sort.asc)
|
||||||
? getArrowDirection(sort.asc)
|
: undefined
|
||||||
: undefined
|
}
|
||||||
|
arrowPosition="right"
|
||||||
|
onClick={() => onSort(PluginListUrlSortField.name)}
|
||||||
|
className={classes.colName}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Name",
|
||||||
|
description: "plugin name"
|
||||||
|
})}
|
||||||
|
</TableCellHeader>
|
||||||
|
<TableCellHeader
|
||||||
|
direction={
|
||||||
|
sort.sort === PluginListUrlSortField.active
|
||||||
|
? getArrowDirection(sort.asc)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onClick={() => onSort(PluginListUrlSortField.active)}
|
||||||
|
className={classes.colActive}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Active",
|
||||||
|
description: "plugin status"
|
||||||
|
})}
|
||||||
|
</TableCellHeader>
|
||||||
|
<TableCell className={classes.colAction}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Action",
|
||||||
|
description: "user action bar"
|
||||||
|
})}
|
||||||
|
</TableCell>
|
||||||
|
</TableHead>
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow>
|
||||||
|
<TablePagination
|
||||||
|
colSpan={numberOfColumns}
|
||||||
|
settings={settings}
|
||||||
|
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
|
||||||
|
onNextPage={onNextPage}
|
||||||
|
onUpdateListSettings={onUpdateListSettings}
|
||||||
|
hasPreviousPage={
|
||||||
|
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
|
||||||
}
|
}
|
||||||
arrowPosition="right"
|
onPreviousPage={onPreviousPage}
|
||||||
onClick={() => onSort(PluginListUrlSortField.name)}
|
/>
|
||||||
className={classes.colName}
|
</TableRow>
|
||||||
>
|
</TableFooter>
|
||||||
{intl.formatMessage({
|
<TableBody>
|
||||||
defaultMessage: "Name",
|
{renderCollection(
|
||||||
description: "plugin name"
|
plugins,
|
||||||
})}
|
plugin => (
|
||||||
</TableCellHeader>
|
<TableRow
|
||||||
<TableCellHeader
|
hover={!!plugin}
|
||||||
direction={
|
className={!!plugin ? classes.link : undefined}
|
||||||
sort.sort === PluginListUrlSortField.active
|
onClick={plugin ? onRowClick(plugin.id) : undefined}
|
||||||
? getArrowDirection(sort.asc)
|
key={plugin ? plugin.id : "skeleton"}
|
||||||
: undefined
|
>
|
||||||
}
|
<TableCell className={classes.colName}>
|
||||||
onClick={() => onSort(PluginListUrlSortField.active)}
|
{maybe<React.ReactNode>(() => plugin.name, <Skeleton />)}
|
||||||
className={classes.colActive}
|
</TableCell>
|
||||||
>
|
<TableCell className={classes.colActive}>
|
||||||
{intl.formatMessage({
|
{maybe<React.ReactNode>(
|
||||||
defaultMessage: "Active",
|
() => (
|
||||||
description: "plugin status"
|
<StatusLabel
|
||||||
})}
|
label={translateBoolean(plugin.active, intl)}
|
||||||
</TableCellHeader>
|
status={plugin.active ? "success" : "error"}
|
||||||
<TableCell className={classes.colAction}>
|
/>
|
||||||
{intl.formatMessage({
|
),
|
||||||
defaultMessage: "Action",
|
<Skeleton />
|
||||||
description: "user action bar"
|
)}
|
||||||
})}
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell className={classes.colAction}>
|
||||||
</TableHead>
|
<div onClick={plugin ? onRowClick(plugin.id) : undefined}>
|
||||||
<TableFooter>
|
<EditIcon />
|
||||||
<TableRow>
|
</div>
|
||||||
<TablePagination
|
</TableCell>
|
||||||
colSpan={numberOfColumns}
|
</TableRow>
|
||||||
settings={settings}
|
),
|
||||||
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
|
() => (
|
||||||
onNextPage={onNextPage}
|
<TableRow>
|
||||||
onUpdateListSettings={onUpdateListSettings}
|
<TableCell colSpan={numberOfColumns}>
|
||||||
hasPreviousPage={
|
{intl.formatMessage({
|
||||||
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
|
defaultMessage: "No plugins found"
|
||||||
}
|
})}
|
||||||
onPreviousPage={onPreviousPage}
|
</TableCell>
|
||||||
/>
|
</TableRow>
|
||||||
</TableRow>
|
)
|
||||||
</TableFooter>
|
)}
|
||||||
<TableBody>
|
</TableBody>
|
||||||
{renderCollection(
|
</ResponsiveTable>
|
||||||
plugins,
|
|
||||||
plugin => (
|
|
||||||
<TableRow
|
|
||||||
hover={!!plugin}
|
|
||||||
className={!!plugin ? classes.link : undefined}
|
|
||||||
onClick={plugin ? onRowClick(plugin.id) : undefined}
|
|
||||||
key={plugin ? plugin.id : "skeleton"}
|
|
||||||
>
|
|
||||||
<TableCell className={classes.colName}>
|
|
||||||
{maybe<React.ReactNode>(() => plugin.name, <Skeleton />)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.colActive}>
|
|
||||||
{maybe<React.ReactNode>(
|
|
||||||
() => (
|
|
||||||
<StatusLabel
|
|
||||||
label={translateBoolean(plugin.active, intl)}
|
|
||||||
status={plugin.active ? "success" : "error"}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
<Skeleton />
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.colAction}>
|
|
||||||
<div onClick={plugin ? onRowClick(plugin.id) : undefined}>
|
|
||||||
<EditIcon />
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
),
|
|
||||||
() => (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={numberOfColumns}>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "No plugins found"
|
|
||||||
})}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</ResponsiveTable>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
PluginList.displayName = "PluginList";
|
PluginList.displayName = "PluginList";
|
||||||
|
|
|
@ -1,35 +1,85 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import Card from "@material-ui/core/Card";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import AppHeader from "@saleor/components/AppHeader";
|
import AppHeader from "@saleor/components/AppHeader";
|
||||||
import Container from "@saleor/components/Container";
|
import Container from "@saleor/components/Container";
|
||||||
import PageHeader from "@saleor/components/PageHeader";
|
import PageHeader from "@saleor/components/PageHeader";
|
||||||
import { sectionNames } from "@saleor/intl";
|
import { sectionNames } from "@saleor/intl";
|
||||||
import { PageListProps, SortPage } from "@saleor/types";
|
import {
|
||||||
|
PageListProps,
|
||||||
|
SortPage,
|
||||||
|
FilterPageProps,
|
||||||
|
TabPageProps
|
||||||
|
} from "@saleor/types";
|
||||||
import { PluginListUrlSortField } from "@saleor/plugins/urls";
|
import { PluginListUrlSortField } from "@saleor/plugins/urls";
|
||||||
|
import FilterBar from "@saleor/components/FilterBar";
|
||||||
import { Plugins_plugins_edges_node } from "../../types/Plugins";
|
import { Plugins_plugins_edges_node } from "../../types/Plugins";
|
||||||
import PluginsList from "../PluginsList/PluginsList";
|
import PluginsList from "../PluginsList/PluginsList";
|
||||||
|
import {
|
||||||
|
createFilterStructure,
|
||||||
|
PluginFilterKeys,
|
||||||
|
PluginListFilterOpts
|
||||||
|
} from "./filters";
|
||||||
|
|
||||||
export interface PluginsListPageProps
|
export interface PluginsListPageProps
|
||||||
extends PageListProps,
|
extends PageListProps,
|
||||||
SortPage<PluginListUrlSortField> {
|
FilterPageProps<PluginFilterKeys, PluginListFilterOpts>,
|
||||||
|
SortPage<PluginListUrlSortField>,
|
||||||
|
TabPageProps {
|
||||||
plugins: Plugins_plugins_edges_node[];
|
plugins: Plugins_plugins_edges_node[];
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PluginsListPage: React.FC<PluginsListPageProps> = ({
|
const PluginsListPage: React.FC<PluginsListPageProps> = ({
|
||||||
|
currencySymbol,
|
||||||
|
currentTab,
|
||||||
|
initialSearch,
|
||||||
|
filterOpts,
|
||||||
|
tabs,
|
||||||
|
onAdd,
|
||||||
|
onAll,
|
||||||
onBack,
|
onBack,
|
||||||
|
onSearchChange,
|
||||||
|
onFilterChange,
|
||||||
|
onTabChange,
|
||||||
|
onTabDelete,
|
||||||
|
onTabSave,
|
||||||
...listProps
|
...listProps
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const filterStructure = createFilterStructure(intl, filterOpts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<AppHeader onBack={onBack}>
|
<AppHeader onBack={onBack}>
|
||||||
{intl.formatMessage(sectionNames.configuration)}
|
{intl.formatMessage(sectionNames.configuration)}
|
||||||
</AppHeader>
|
</AppHeader>
|
||||||
<PageHeader title={intl.formatMessage(sectionNames.plugins)} />
|
<PageHeader title={intl.formatMessage(sectionNames.plugins)} />
|
||||||
<PluginsList {...listProps} />
|
<Card>
|
||||||
|
<FilterBar
|
||||||
|
currencySymbol={currencySymbol}
|
||||||
|
currentTab={currentTab}
|
||||||
|
initialSearch={initialSearch}
|
||||||
|
onAll={onAll}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
|
onSearchChange={onSearchChange}
|
||||||
|
onTabChange={onTabChange}
|
||||||
|
onTabDelete={onTabDelete}
|
||||||
|
onTabSave={onTabSave}
|
||||||
|
tabs={tabs}
|
||||||
|
allTabLabel={intl.formatMessage({
|
||||||
|
defaultMessage: "All Plugins",
|
||||||
|
description: "tab name"
|
||||||
|
})}
|
||||||
|
filterStructure={filterStructure}
|
||||||
|
searchPlaceholder={intl.formatMessage({
|
||||||
|
defaultMessage: "Search Plugins..."
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<PluginsList {...listProps} />
|
||||||
|
</Card>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
45
src/plugins/components/PluginsListPage/filters.ts
Normal file
45
src/plugins/components/PluginsListPage/filters.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { defineMessages, IntlShape } from "react-intl";
|
||||||
|
|
||||||
|
import { FilterOpts } from "@saleor/types";
|
||||||
|
import { IFilter } from "@saleor/components/Filter";
|
||||||
|
import { createBooleanField } from "@saleor/utils/filters/fields";
|
||||||
|
import { commonMessages } from "@saleor/intl";
|
||||||
|
|
||||||
|
export enum PluginFilterKeys {
|
||||||
|
active = "active"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginListFilterOpts {
|
||||||
|
isActive: FilterOpts<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
active: {
|
||||||
|
defaultMessage: "Active",
|
||||||
|
description: "plugin"
|
||||||
|
},
|
||||||
|
deactivated: {
|
||||||
|
defaultMessage: "Inactive",
|
||||||
|
description: "plugin"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createFilterStructure(
|
||||||
|
intl: IntlShape,
|
||||||
|
opts: PluginListFilterOpts
|
||||||
|
): IFilter<PluginFilterKeys> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...createBooleanField(
|
||||||
|
PluginFilterKeys.active,
|
||||||
|
intl.formatMessage(commonMessages.status),
|
||||||
|
opts.isActive.value,
|
||||||
|
{
|
||||||
|
negative: intl.formatMessage(messages.deactivated),
|
||||||
|
positive: intl.formatMessage(messages.active)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
active: opts.isActive.active
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
export { default } from "./PluginsListPage";
|
export { default } from "./PluginsListPage";
|
||||||
export * from "./PluginsListPage";
|
export * from "./PluginsListPage";
|
||||||
|
export * from "./filters";
|
||||||
|
|
|
@ -35,6 +35,7 @@ const pluginsList = gql`
|
||||||
$after: String
|
$after: String
|
||||||
$last: Int
|
$last: Int
|
||||||
$before: String
|
$before: String
|
||||||
|
$filter: PluginFilterInput
|
||||||
$sort: PluginSortingInput
|
$sort: PluginSortingInput
|
||||||
) {
|
) {
|
||||||
plugins(
|
plugins(
|
||||||
|
@ -42,6 +43,7 @@ const pluginsList = gql`
|
||||||
after: $after
|
after: $after
|
||||||
first: $first
|
first: $first
|
||||||
last: $last
|
last: $last
|
||||||
|
filter: $filter
|
||||||
sortBy: $sort
|
sortBy: $sort
|
||||||
) {
|
) {
|
||||||
edges {
|
edges {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// This file was automatically generated and should not be edited.
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
import { PluginSortingInput } from "./../../types/globalTypes";
|
import { PluginFilterInput, PluginSortingInput } from "./../../types/globalTypes";
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// GraphQL query operation: Plugins
|
// GraphQL query operation: Plugins
|
||||||
|
@ -44,5 +44,6 @@ export interface PluginsVariables {
|
||||||
after?: string | null;
|
after?: string | null;
|
||||||
last?: number | null;
|
last?: number | null;
|
||||||
before?: string | null;
|
before?: string | null;
|
||||||
|
filter?: PluginFilterInput | null;
|
||||||
sort?: PluginSortingInput | null;
|
sort?: PluginSortingInput | null;
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue