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
|
||||
- Throw error when API_URI is not set - #375 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
|
||||
|
||||
## 2.0.0
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -164,6 +164,7 @@
|
|||
"@assets(.*)$": "<rootDir>/assets/$1",
|
||||
"@locale(.*)$": "<rootDir>/locale/$1",
|
||||
"@saleor(.*)$": "<rootDir>/src/$1",
|
||||
"@test/(.*)$": "<rootDir>/testUtils/$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" {
|
||||
import { OptionalIntlConfig } from "react-intl/dist/components/provider";
|
||||
import * as ReactIntl from "node_modules/react-intl";
|
||||
export * from "node_modules/react-intl";
|
||||
|
||||
|
@ -51,4 +52,6 @@ declare module "react-intl" {
|
|||
> extends React.Component<FormattedMessageProps<TValues>> {}
|
||||
|
||||
export function useIntl(): IntlShape;
|
||||
|
||||
export function createIntl(config: OptionalIntlConfig): IntlShape;
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ input AccountInput {
|
|||
|
||||
type AccountRegister {
|
||||
errors: [Error!]
|
||||
requiresConfirmation: Boolean!
|
||||
requiresConfirmation: Boolean
|
||||
accountErrors: [AccountError!]
|
||||
user: User
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import React from "react";
|
|||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import AppHeader from "@saleor/components/AppHeader";
|
||||
import SearchBar from "@saleor/components/SearchBar";
|
||||
import FilterBar from "@saleor/components/FilterBar";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import { AttributeListUrlSortField } from "@saleor/attributes/urls";
|
||||
import Container from "../../../components/Container";
|
||||
|
@ -12,17 +12,22 @@ import PageHeader from "../../../components/PageHeader";
|
|||
import {
|
||||
ListActions,
|
||||
PageListProps,
|
||||
SearchPageProps,
|
||||
FilterPageProps,
|
||||
TabPageProps,
|
||||
SortPage
|
||||
} from "../../../types";
|
||||
import { AttributeList_attributes_edges_node } from "../../types/AttributeList";
|
||||
import AttributeList from "../AttributeList/AttributeList";
|
||||
import {
|
||||
createFilterStructure,
|
||||
AttributeListFilterOpts,
|
||||
AttributeFilterKeys
|
||||
} from "./filters";
|
||||
|
||||
export interface AttributeListPageProps
|
||||
extends PageListProps,
|
||||
ListActions,
|
||||
SearchPageProps,
|
||||
FilterPageProps<AttributeFilterKeys, AttributeListFilterOpts>,
|
||||
SortPage<AttributeListUrlSortField>,
|
||||
TabPageProps {
|
||||
attributes: AttributeList_attributes_edges_node[];
|
||||
|
@ -30,9 +35,12 @@ export interface AttributeListPageProps
|
|||
}
|
||||
|
||||
const AttributeListPage: React.FC<AttributeListPageProps> = ({
|
||||
currencySymbol,
|
||||
filterOpts,
|
||||
initialSearch,
|
||||
onAdd,
|
||||
onBack,
|
||||
initialSearch,
|
||||
onFilterChange,
|
||||
onSearchChange,
|
||||
currentTab,
|
||||
onAll,
|
||||
|
@ -44,6 +52,8 @@ const AttributeListPage: React.FC<AttributeListPageProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const structure = createFilterStructure(intl, filterOpts);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<AppHeader onBack={onBack}>
|
||||
|
@ -58,18 +68,21 @@ const AttributeListPage: React.FC<AttributeListPageProps> = ({
|
|||
</Button>
|
||||
</PageHeader>
|
||||
<Card>
|
||||
<SearchBar
|
||||
<FilterBar
|
||||
allTabLabel={intl.formatMessage({
|
||||
defaultMessage: "All Attributes",
|
||||
description: "tab name"
|
||||
})}
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterStructure={structure}
|
||||
initialSearch={initialSearch}
|
||||
searchPlaceholder={intl.formatMessage({
|
||||
defaultMessage: "Search Attribute"
|
||||
})}
|
||||
tabs={tabs}
|
||||
onAll={onAll}
|
||||
onFilterChange={onFilterChange}
|
||||
onSearchChange={onSearchChange}
|
||||
onTabChange={onTabChange}
|
||||
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 * from "./AttributeListPage";
|
||||
export * from "./filters";
|
||||
|
|
|
@ -15,6 +15,12 @@ import {
|
|||
export const attributeSection = "/attributes/";
|
||||
|
||||
export enum AttributeListUrlFiltersEnum {
|
||||
availableInGrid = "availableInGrid",
|
||||
filterableInDashboard = "filterableInDashboard",
|
||||
filterableInStorefront = "filterableInStorefront",
|
||||
isVariantOnly = "isVariantOnly",
|
||||
valueRequired = "valueRequired",
|
||||
visibleInStorefront = "visibleInStorefront",
|
||||
query = "query"
|
||||
}
|
||||
export type AttributeListUrlFilters = Filters<AttributeListUrlFiltersEnum>;
|
||||
|
|
|
@ -9,7 +9,8 @@ import {
|
|||
getActiveFilters,
|
||||
getFilterTabs,
|
||||
getFilterVariables,
|
||||
saveFilterTab
|
||||
saveFilterTab,
|
||||
getFilterOpts
|
||||
} from "@saleor/attributes/views/AttributeList/filters";
|
||||
import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
|
||||
import SaveFilterTabDialog, {
|
||||
|
@ -24,6 +25,8 @@ import usePaginator, {
|
|||
import { getSortParams } from "@saleor/utils/sort";
|
||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||
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 useBulkActions from "../../../hooks/useBulkActions";
|
||||
import { maybe } from "../../../misc";
|
||||
|
@ -35,12 +38,12 @@ import { AttributeBulkDelete } from "../../types/AttributeBulkDelete";
|
|||
import {
|
||||
attributeAddUrl,
|
||||
attributeListUrl,
|
||||
AttributeListUrlFilters,
|
||||
AttributeListUrlQueryParams,
|
||||
attributeUrl,
|
||||
AttributeListUrlDialog
|
||||
} from "../../urls";
|
||||
import { getSortQueryVariables } from "./sort";
|
||||
import { getFilterQueryParam } from "./filters";
|
||||
|
||||
interface AttributeListProps {
|
||||
params: AttributeListUrlQueryParams;
|
||||
|
@ -50,6 +53,7 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
|
|||
const navigate = useNavigator();
|
||||
const paginate = usePaginator();
|
||||
const notify = useNotifier();
|
||||
const shop = useShop();
|
||||
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
||||
params.ids
|
||||
);
|
||||
|
@ -82,16 +86,17 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
|
|||
AttributeListUrlQueryParams
|
||||
>(navigate, attributeListUrl, params);
|
||||
|
||||
const changeFilterField = (filter: AttributeListUrlFilters) => {
|
||||
reset();
|
||||
navigate(
|
||||
attributeListUrl({
|
||||
...getActiveFilters(params),
|
||||
...filter,
|
||||
activeTab: undefined
|
||||
})
|
||||
);
|
||||
};
|
||||
const [
|
||||
changeFilters,
|
||||
resetFilters,
|
||||
handleSearchChange
|
||||
] = createFilterHandlers({
|
||||
cleanupFn: reset,
|
||||
createUrl: attributeListUrl,
|
||||
getFilterQueryParam,
|
||||
navigate,
|
||||
params
|
||||
});
|
||||
|
||||
const handleTabChange = (tab: number) => {
|
||||
reset();
|
||||
|
@ -135,6 +140,7 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
|
|||
};
|
||||
|
||||
const handleSort = createSortHandler(navigate, attributeListUrl, params);
|
||||
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||
|
||||
return (
|
||||
<AttributeBulkDeleteMutation onCompleted={handleBulkDelete}>
|
||||
|
@ -144,17 +150,20 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
|
|||
attributes={maybe(() =>
|
||||
data.attributes.edges.map(edge => edge.node)
|
||||
)}
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
disabled={loading || attributeBulkDeleteOpts.loading}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
initialSearch={params.query || ""}
|
||||
isChecked={isSelected}
|
||||
onAdd={() => navigate(attributeAddUrl())}
|
||||
onAll={() => navigate(attributeListUrl())}
|
||||
onAll={resetFilters}
|
||||
onBack={() => navigate(configurationMenuUrl)}
|
||||
onFilterChange={changeFilters}
|
||||
onNextPage={loadNextPage}
|
||||
onPreviousPage={loadPreviousPage}
|
||||
onRowClick={id => () => navigate(attributeUrl(id))}
|
||||
onSearchChange={query => changeFilterField({ query })}
|
||||
onSearchChange={handleSearchChange}
|
||||
onSort={handleSort}
|
||||
onTabChange={handleTabChange}
|
||||
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 { maybe, parseBoolean } from "@saleor/misc";
|
||||
import { IFilterElement } from "@saleor/components/Filter";
|
||||
import {
|
||||
AttributeListFilterOpts,
|
||||
AttributeFilterKeys
|
||||
} from "@saleor/attributes/components/AttributeListPage";
|
||||
import {
|
||||
createFilterTabUtils,
|
||||
createFilterUtils
|
||||
createFilterUtils,
|
||||
getSingleValueQueryParam
|
||||
} from "../../../utils/filters";
|
||||
import {
|
||||
AttributeListUrlFilters,
|
||||
|
@ -11,14 +18,113 @@ import {
|
|||
|
||||
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(
|
||||
params: AttributeListUrlFilters
|
||||
): AttributeFilterInput {
|
||||
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 {
|
||||
deleteFilterTab,
|
||||
getFilterTabs,
|
||||
|
|
|
@ -5,34 +5,42 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||
|
||||
import { Container } from "@saleor/components/Container";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import SearchBar from "@saleor/components/SearchBar";
|
||||
import FilterBar from "@saleor/components/FilterBar";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import {
|
||||
ListActions,
|
||||
PageListProps,
|
||||
SearchPageProps,
|
||||
FilterPageProps,
|
||||
TabPageProps,
|
||||
SortPage
|
||||
} from "@saleor/types";
|
||||
import { CollectionListUrlSortField } from "@saleor/collections/urls";
|
||||
import { CollectionList_collections_edges_node } from "../../types/CollectionList";
|
||||
import CollectionList from "../CollectionList/CollectionList";
|
||||
import {
|
||||
CollectionFilterKeys,
|
||||
CollectionListFilterOpts,
|
||||
createFilterStructure
|
||||
} from "./filters";
|
||||
|
||||
export interface CollectionListPageProps
|
||||
extends PageListProps,
|
||||
ListActions,
|
||||
SearchPageProps,
|
||||
FilterPageProps<CollectionFilterKeys, CollectionListFilterOpts>,
|
||||
SortPage<CollectionListUrlSortField>,
|
||||
TabPageProps {
|
||||
collections: CollectionList_collections_edges_node[];
|
||||
}
|
||||
|
||||
const CollectionListPage: React.FC<CollectionListPageProps> = ({
|
||||
currencySymbol,
|
||||
currentTab,
|
||||
disabled,
|
||||
filterOpts,
|
||||
initialSearch,
|
||||
onAdd,
|
||||
onAll,
|
||||
onFilterChange,
|
||||
onSearchChange,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
|
@ -42,6 +50,8 @@ const CollectionListPage: React.FC<CollectionListPageProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const structure = createFilterStructure(intl, filterOpts);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<PageHeader title={intl.formatMessage(sectionNames.collections)}>
|
||||
|
@ -58,18 +68,21 @@ const CollectionListPage: React.FC<CollectionListPageProps> = ({
|
|||
</Button>
|
||||
</PageHeader>
|
||||
<Card>
|
||||
<SearchBar
|
||||
<FilterBar
|
||||
allTabLabel={intl.formatMessage({
|
||||
defaultMessage: "All Collections",
|
||||
description: "tab name"
|
||||
})}
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterStructure={structure}
|
||||
initialSearch={initialSearch}
|
||||
searchPlaceholder={intl.formatMessage({
|
||||
defaultMessage: "Search Collection"
|
||||
})}
|
||||
tabs={tabs}
|
||||
onAll={onAll}
|
||||
onFilterChange={onFilterChange}
|
||||
onSearchChange={onSearchChange}
|
||||
onTabChange={onTabChange}
|
||||
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 * from "./CollectionListPage";
|
||||
export * from "./filters";
|
||||
|
|
|
@ -15,6 +15,7 @@ const collectionSectionUrl = "/collections/";
|
|||
|
||||
export const collectionListPath = collectionSectionUrl;
|
||||
export enum CollectionListUrlFiltersEnum {
|
||||
status = "status",
|
||||
query = "query"
|
||||
}
|
||||
export type CollectionListUrlFilters = Filters<CollectionListUrlFiltersEnum>;
|
||||
|
|
|
@ -23,6 +23,8 @@ import { ListViews } from "@saleor/types";
|
|||
import { getSortParams } from "@saleor/utils/sort";
|
||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||
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 {
|
||||
TypedCollectionBulkDelete,
|
||||
|
@ -34,7 +36,6 @@ import { CollectionBulkPublish } from "../../types/CollectionBulkPublish";
|
|||
import {
|
||||
collectionAddUrl,
|
||||
collectionListUrl,
|
||||
CollectionListUrlFilters,
|
||||
CollectionListUrlQueryParams,
|
||||
collectionUrl,
|
||||
CollectionListUrlDialog
|
||||
|
@ -45,8 +46,10 @@ import {
|
|||
getActiveFilters,
|
||||
getFilterTabs,
|
||||
getFilterVariables,
|
||||
saveFilterTab
|
||||
} from "./filter";
|
||||
saveFilterTab,
|
||||
getFilterQueryParam,
|
||||
getFilterOpts
|
||||
} from "./filters";
|
||||
import { getSortQueryVariables } from "./sort";
|
||||
|
||||
interface CollectionListProps {
|
||||
|
@ -57,6 +60,7 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
|
|||
const navigate = useNavigator();
|
||||
const notify = useNotifier();
|
||||
const paginate = usePaginator();
|
||||
const shop = useShop();
|
||||
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
||||
params.ids
|
||||
);
|
||||
|
@ -88,16 +92,17 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
|
|||
: 0
|
||||
: parseInt(params.activeTab, 0);
|
||||
|
||||
const changeFilterField = (filter: CollectionListUrlFilters) => {
|
||||
reset();
|
||||
navigate(
|
||||
collectionListUrl({
|
||||
...getActiveFilters(params),
|
||||
...filter,
|
||||
activeTab: undefined
|
||||
})
|
||||
);
|
||||
};
|
||||
const [
|
||||
changeFilters,
|
||||
resetFilters,
|
||||
handleSearchChange
|
||||
] = createFilterHandlers({
|
||||
cleanupFn: reset,
|
||||
createUrl: collectionListUrl,
|
||||
getFilterQueryParam,
|
||||
navigate,
|
||||
params
|
||||
});
|
||||
|
||||
const [openModal, closeModal] = createDialogActionHandlers<
|
||||
CollectionListUrlDialog,
|
||||
|
@ -154,6 +159,7 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
|
|||
};
|
||||
|
||||
const handleSort = createSortHandler(navigate, collectionListUrl, params);
|
||||
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||
|
||||
return (
|
||||
<TypedCollectionBulkDelete onCompleted={handleCollectionBulkDelete}>
|
||||
|
@ -162,11 +168,14 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
|
|||
{(collectionBulkPublish, collectionBulkPublishOpts) => (
|
||||
<>
|
||||
<CollectionListPage
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
initialSearch={params.query || ""}
|
||||
onSearchChange={query => changeFilterField({ query })}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterChange={changeFilters}
|
||||
onAdd={() => navigate(collectionAddUrl)}
|
||||
onAll={() => navigate(collectionListUrl())}
|
||||
onAll={resetFilters}
|
||||
onTabChange={handleTabChange}
|
||||
onTabDelete={() => openModal("delete-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,
|
||||
gridColumn: 2,
|
||||
height: 70,
|
||||
position: "sticky",
|
||||
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 ClickAwayListener from "@material-ui/core/ClickAwayListener";
|
||||
import Grow from "@material-ui/core/Grow";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Popper from "@material-ui/core/Popper";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { FilterContentSubmitData } from "./FilterContent";
|
||||
import { IFilter } from "./types";
|
||||
import { IFilter, IFilterElement } from "./types";
|
||||
import useFilter from "./useFilter";
|
||||
import { FilterContent } from ".";
|
||||
|
||||
export interface FilterProps<TFilterKeys = string> {
|
||||
export interface FilterProps<TFilterKeys extends string = string> {
|
||||
currencySymbol: string;
|
||||
menu: IFilter<TFilterKeys>;
|
||||
filterLabel: string;
|
||||
onFilterAdd: (filter: FilterContentSubmitData) => void;
|
||||
onFilterAdd: (filter: Array<IFilterElement<string>>) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
|
@ -49,7 +47,6 @@ const useStyles = makeStyles(
|
|||
color: theme.palette.primary.main,
|
||||
fontSize: 14,
|
||||
fontWeight: 600 as 600,
|
||||
marginRight: 4,
|
||||
textTransform: "uppercase"
|
||||
},
|
||||
filterButton: {
|
||||
|
@ -73,38 +70,62 @@ const useStyles = makeStyles(
|
|||
width: 240
|
||||
},
|
||||
popover: {
|
||||
zIndex: 1
|
||||
width: 376,
|
||||
zIndex: 3
|
||||
},
|
||||
rotate: {
|
||||
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" }
|
||||
);
|
||||
const Filter: React.FC<FilterProps> = props => {
|
||||
const { currencySymbol, filterLabel, menu, onFilterAdd } = props;
|
||||
const { currencySymbol, menu, onFilterAdd } = props;
|
||||
const classes = useStyles(props);
|
||||
|
||||
const anchor = React.useRef<HTMLDivElement>();
|
||||
const [isFilterMenuOpened, setFilterMenuOpened] = React.useState(false);
|
||||
const [data, dispatch, reset] = useFilter(menu);
|
||||
|
||||
const isFilterActive = menu.some(filterElement => filterElement.active);
|
||||
|
||||
return (
|
||||
<ClickAwayListener
|
||||
onClickAway={event => {
|
||||
if ((event.target as HTMLElement).getAttribute("role") !== "option") {
|
||||
setFilterMenuOpened(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div ref={anchor}>
|
||||
<ButtonBase
|
||||
className={classNames(classes.filterButton, classes.addFilterButton, {
|
||||
[classes.addFilterButtonActive]: isFilterMenuOpened
|
||||
[classes.addFilterButtonActive]:
|
||||
isFilterMenuOpened || isFilterActive
|
||||
})}
|
||||
onClick={() => setFilterMenuOpened(!isFilterMenuOpened)}
|
||||
>
|
||||
<Typography className={classes.addFilterText}>
|
||||
<FormattedMessage defaultMessage="Add Filter" description="button" />
|
||||
<FormattedMessage defaultMessage="Filters" description="button" />
|
||||
</Typography>
|
||||
<ArrowDropDownIcon
|
||||
color="primary"
|
||||
className={classNames(classes.addFilterIcon, {
|
||||
[classes.rotate]: isFilterMenuOpened
|
||||
})}
|
||||
/>
|
||||
{isFilterActive && (
|
||||
<>
|
||||
<span className={classes.separator} />
|
||||
<Typography className={classes.addFilterText}>
|
||||
{menu.reduce(
|
||||
(acc, filterElement) => acc + (filterElement.active ? 1 : 0),
|
||||
0
|
||||
)}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</ButtonBase>
|
||||
<Popper
|
||||
className={classes.popover}
|
||||
|
@ -113,6 +134,18 @@ const Filter: React.FC<FilterProps> = props => {
|
|||
transition
|
||||
disablePortal
|
||||
placement="bottom-start"
|
||||
modifiers={{
|
||||
flip: {
|
||||
enabled: false
|
||||
},
|
||||
hide: {
|
||||
enabled: false
|
||||
},
|
||||
preventOverflow: {
|
||||
boundariesElement: "scrollParent",
|
||||
enabled: false
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
|
@ -122,21 +155,21 @@ const Filter: React.FC<FilterProps> = props => {
|
|||
placement === "bottom" ? "right top" : "right bottom"
|
||||
}}
|
||||
>
|
||||
<Paper className={classes.paper}>
|
||||
<Typography>{filterLabel}</Typography>
|
||||
<FilterContent
|
||||
currencySymbol={currencySymbol}
|
||||
filters={menu}
|
||||
onSubmit={data => {
|
||||
filters={data}
|
||||
onClear={reset}
|
||||
onFilterPropertyChange={dispatch}
|
||||
onSubmit={() => {
|
||||
onFilterAdd(data);
|
||||
setFilterMenuOpened(false);
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
);
|
||||
};
|
||||
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 Paper from "@material-ui/core/Paper";
|
||||
import Radio from "@material-ui/core/Radio";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||
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 { getMenuItemByValue, isLeaf, walkToRoot } from "../../utils/menu";
|
||||
import FormSpacer from "../FormSpacer";
|
||||
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
||||
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 FilterElement from "./FilterElement";
|
||||
import { IFilter } from "./types";
|
||||
import { SingleAutocompleteChoiceType } from "../SingleAutocompleteSelectField";
|
||||
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> {
|
||||
name: TKeys;
|
||||
value: string | string[];
|
||||
}
|
||||
export interface FilterContentProps {
|
||||
export interface FilterContentProps<T extends string = string> {
|
||||
currencySymbol: string;
|
||||
filters: IFilter<string>;
|
||||
onSubmit: (data: FilterContentSubmitData) => 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()
|
||||
}));
|
||||
filters: IFilter<T>;
|
||||
onFilterPropertyChange: React.Dispatch<FilterReducerAction<T>>;
|
||||
onClear: () => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
{
|
||||
input: {
|
||||
padding: "20px 12px 17px"
|
||||
}
|
||||
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: {
|
||||
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" }
|
||||
);
|
||||
|
||||
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> = ({
|
||||
currencySymbol,
|
||||
filters,
|
||||
onClear,
|
||||
onFilterPropertyChange,
|
||||
onSubmit
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [menuValue, setMenuValue] = React.useState<string>(null);
|
||||
const [filterValue, setFilterValue] = React.useState<string | string[]>("");
|
||||
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
|
||||
? 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 acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SingleSelectField
|
||||
choices={getFilterChoices(filters)}
|
||||
onChange={onMenuChange}
|
||||
selectProps={{
|
||||
classes: {
|
||||
selectMenu: classes.input
|
||||
}
|
||||
<Paper>
|
||||
<form
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
value={menus ? menus[0].value : menuValue}
|
||||
placeholder={intl.formatMessage({
|
||||
defaultMessage: "Select Filter..."
|
||||
})}
|
||||
/>
|
||||
{menus &&
|
||||
menus.map(
|
||||
(filterItem, filterItemIndex) =>
|
||||
!isLeaf(filterItem) && (
|
||||
<React.Fragment
|
||||
key={filterItem.label.toString() + ":" + filterItem.value}
|
||||
>
|
||||
<FormSpacer />
|
||||
<SingleSelectField
|
||||
choices={getFilterChoices(filterItem.children)}
|
||||
onChange={onMenuChange}
|
||||
selectProps={{
|
||||
classes: {
|
||||
selectMenu: classes.input
|
||||
<div className={classes.actionBar}>
|
||||
<Typography className={classes.label}>
|
||||
<FormattedMessage defaultMessage="Filters" />
|
||||
</Typography>
|
||||
<div>
|
||||
<Button className={classes.clear} onClick={onClear}>
|
||||
<FormattedMessage {...buttonMessages.clear} />
|
||||
</Button>
|
||||
<Button color="primary" variant="contained" type="submit">
|
||||
<FormattedMessage {...buttonMessages.done} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Hr />
|
||||
{filters
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||
.map(filterField => (
|
||||
<React.Fragment key={filterField.name}>
|
||||
<div className={classes.filterFieldBar}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={filterField.active} />}
|
||||
label={filterField.label}
|
||||
onChange={() =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
active: !filterField.active
|
||||
}
|
||||
}}
|
||||
value={
|
||||
filterItemIndex === menus.length - 1
|
||||
? menuValue.toString()
|
||||
: menus[filterItemIndex - 1].label.toString()
|
||||
}
|
||||
placeholder={intl.formatMessage({
|
||||
defaultMessage: "Select Filter..."
|
||||
})}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
{activeMenu && isLeaf(activeMenu) && (
|
||||
<>
|
||||
<FormSpacer />
|
||||
{activeMenu.data.additionalText && (
|
||||
<Typography>{activeMenu.data.additionalText}</Typography>
|
||||
)}
|
||||
<FilterElement
|
||||
currencySymbol={currencySymbol}
|
||||
filter={activeMenu}
|
||||
value={filterValue}
|
||||
onChange={value => setFilterValue(value)}
|
||||
/>
|
||||
{checkFilterValue(filterValue) && (
|
||||
<>
|
||||
<FormSpacer />
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() =>
|
||||
onSubmit({
|
||||
name: activeMenu.value,
|
||||
value: filterValue
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add filter"
|
||||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
{filterField.active && (
|
||||
<div className={classes.filterSettings}>
|
||||
{filterField.type === FieldType.text && (
|
||||
<TextField
|
||||
fullWidth
|
||||
name={filterField.name}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
}
|
||||
}}
|
||||
value={filterField.value[0]}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
value: [event.target.value, filterField.value[1]]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{[FieldType.date, FieldType.price, FieldType.number].includes(
|
||||
filterField.type
|
||||
) && (
|
||||
<>
|
||||
<SingleSelectField
|
||||
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";
|
||||
|
|
|
@ -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 {
|
||||
autocomplete,
|
||||
boolean,
|
||||
date,
|
||||
hidden,
|
||||
dateTime,
|
||||
number,
|
||||
price,
|
||||
range,
|
||||
rangeDate,
|
||||
rangePrice,
|
||||
select,
|
||||
options,
|
||||
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;
|
||||
value: string | boolean;
|
||||
}
|
||||
|
||||
export interface FilterData {
|
||||
additionalText?: string;
|
||||
fieldLabel: string;
|
||||
options?: FilterChoice[];
|
||||
name: T;
|
||||
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 { 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 Debounce from "../Debounce";
|
||||
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 {
|
||||
filterMenu: IFilter<TKeys>;
|
||||
export interface FilterBarProps<TKeys extends string = string>
|
||||
extends FilterProps<TKeys>,
|
||||
SearchBarProps {
|
||||
filterStructure: IFilter<TKeys>;
|
||||
}
|
||||
|
||||
const FilterBar: React.FC<FilterBarProps> = ({
|
||||
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: "FilterBar"
|
||||
}
|
||||
);
|
||||
|
||||
const FilterBar: React.FC<FilterBarProps> = props => {
|
||||
const {
|
||||
allTabLabel,
|
||||
currencySymbol,
|
||||
filterLabel,
|
||||
filtersList,
|
||||
filterMenu,
|
||||
filterStructure,
|
||||
currentTab,
|
||||
initialSearch,
|
||||
searchPlaceholder,
|
||||
tabs,
|
||||
onAll,
|
||||
onSearchChange,
|
||||
onFilterAdd,
|
||||
onFilterChange,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
onTabSave
|
||||
}) => {
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
const [search, setSearch] = React.useState(initialSearch);
|
||||
React.useEffect(() => setSearch(initialSearch), [currentTab, initialSearch]);
|
||||
|
||||
const isCustom = currentTab === tabs.length + 1;
|
||||
const displayTabAction = isCustom
|
||||
? "save"
|
||||
: currentTab === 0
|
||||
? null
|
||||
: "delete";
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -53,34 +82,44 @@ const FilterBar: React.FC<FilterBarProps> = ({
|
|||
/>
|
||||
)}
|
||||
</FilterTabs>
|
||||
<Debounce debounceFn={onSearchChange}>
|
||||
{debounceSearchChange => {
|
||||
const handleSearchChange = (event: React.ChangeEvent<any>) => {
|
||||
const value = event.target.value;
|
||||
setSearch(value);
|
||||
debounceSearchChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<FilterChips
|
||||
<div className={classes.root}>
|
||||
<Filter
|
||||
currencySymbol={currencySymbol}
|
||||
displayTabAction={
|
||||
!!initialSearch ? (isCustom ? "save" : "delete") : null
|
||||
}
|
||||
menu={filterMenu}
|
||||
filtersList={filtersList}
|
||||
filterLabel={filterLabel}
|
||||
placeholder={searchPlaceholder}
|
||||
search={search}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterAdd={onFilterAdd}
|
||||
onFilterSave={onTabSave}
|
||||
isCustomSearch={isCustom}
|
||||
onFilterDelete={onTabDelete}
|
||||
menu={filterStructure}
|
||||
onFilterAdd={onFilterChange}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Debounce>
|
||||
<SearchInput
|
||||
initialSearch={initialSearch}
|
||||
placeholder={searchPlaceholder}
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
{displayTabAction &&
|
||||
(displayTabAction === "save" ? (
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -44,7 +44,7 @@ const Link: React.FC<LinkProps> = props => {
|
|||
return (
|
||||
<Typography
|
||||
component="a"
|
||||
className={classNames({
|
||||
className={classNames(className, {
|
||||
[classes.root]: true,
|
||||
[classes[color]]: true,
|
||||
[classes.underline]: underline
|
||||
|
|
|
@ -26,18 +26,16 @@ const props: MultiAutocompleteSelectFieldProps = {
|
|||
value: undefined
|
||||
};
|
||||
|
||||
const Story: React.FC<
|
||||
Partial<
|
||||
const Story: React.FC<Partial<
|
||||
MultiAutocompleteSelectFieldProps & {
|
||||
enableLoadMore: boolean;
|
||||
}
|
||||
>
|
||||
> = ({ allowCustomValues, enableLoadMore }) => {
|
||||
>> = ({ allowCustomValues, enableLoadMore }) => {
|
||||
const { change, data: countries } = useMultiAutocomplete([suggestions[0]]);
|
||||
|
||||
return (
|
||||
<ChoiceProvider choices={suggestions}>
|
||||
{({ choices, fetchChoices, fetchMore, hasMore, loading }) => (
|
||||
{({ choices, fetchChoices, onFetchMore, hasMore, loading }) => (
|
||||
<MultiAutocompleteSelectField
|
||||
{...props}
|
||||
displayValues={countries}
|
||||
|
@ -50,7 +48,7 @@ const Story: React.FC<
|
|||
value={countries.map(country => country.value)}
|
||||
loading={loading}
|
||||
hasMore={enableLoadMore ? hasMore : false}
|
||||
onFetchMore={enableLoadMore ? fetchMore : undefined}
|
||||
onFetchMore={enableLoadMore ? onFetchMore : undefined}
|
||||
allowCustomValues={allowCustomValues}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -41,7 +41,7 @@ const useStyles = makeStyles(
|
|||
background: theme.palette.background.default,
|
||||
borderTop: "1px solid transparent",
|
||||
boxShadow: `0 -5px 5px 0 ${theme.palette.divider}`,
|
||||
height: "100%",
|
||||
height: 70,
|
||||
transition: `box-shadow ${theme.transitions.duration.shortest}ms`
|
||||
},
|
||||
spacer: {
|
||||
|
|
|
@ -1,15 +1,36 @@
|
|||
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 FilterSearch from "../Filter/FilterSearch";
|
||||
import FilterTabs, { FilterTab } from "../TableFilter";
|
||||
import SearchInput from "./SearchInput";
|
||||
|
||||
export interface SearchBarProps extends SearchPageProps, TabPageProps {
|
||||
allTabLabel: 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 {
|
||||
allTabLabel,
|
||||
|
@ -23,9 +44,16 @@ const SearchBar: React.FC<SearchBarProps> = props => {
|
|||
onTabDelete,
|
||||
onTabSave
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
|
||||
const isCustom = currentTab === tabs.length + 1;
|
||||
const displayTabAction = isCustom
|
||||
? "save"
|
||||
: currentTab === 0
|
||||
? null
|
||||
: "delete";
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -47,16 +75,39 @@ const SearchBar: React.FC<SearchBarProps> = props => {
|
|||
/>
|
||||
)}
|
||||
</FilterTabs>
|
||||
<FilterSearch
|
||||
displaySearchAction={
|
||||
!!initialSearch ? (isCustom ? "save" : "delete") : null
|
||||
}
|
||||
<div className={classes.root}>
|
||||
<SearchInput
|
||||
initialSearch={initialSearch}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
placeholder={searchPlaceholder}
|
||||
onSearchChange={onSearchChange}
|
||||
onSearchDelete={onTabDelete}
|
||||
onSearchSave={onTabSave}
|
||||
/>
|
||||
{displayTabAction &&
|
||||
(displayTabAction === "save" ? (
|
||||
<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
|
||||
};
|
||||
|
||||
const Story: React.FC<
|
||||
Partial<
|
||||
const Story: React.FC<Partial<
|
||||
SingleAutocompleteSelectFieldProps & {
|
||||
enableLoadMore: boolean;
|
||||
}
|
||||
>
|
||||
> = ({ allowCustomValues, emptyOption, enableLoadMore }) => {
|
||||
>> = ({ allowCustomValues, emptyOption, enableLoadMore }) => {
|
||||
const [displayValue, setDisplayValue] = React.useState(suggestions[0].label);
|
||||
|
||||
return (
|
||||
<Form initial={{ country: suggestions[0].value }}>
|
||||
{({ change, data }) => (
|
||||
<ChoiceProvider choices={suggestions}>
|
||||
{({ choices, fetchChoices, fetchMore, hasMore, loading }) => {
|
||||
{({ choices, fetchChoices, onFetchMore, hasMore, loading }) => {
|
||||
const handleSelect = createSingleAutocompleteSelectHandler(
|
||||
change,
|
||||
setDisplayValue,
|
||||
|
@ -58,7 +56,7 @@ const Story: React.FC<
|
|||
onChange={handleSelect}
|
||||
value={data.country}
|
||||
hasMore={enableLoadMore ? hasMore : false}
|
||||
onFetchMore={enableLoadMore ? fetchMore : undefined}
|
||||
onFetchMore={enableLoadMore ? onFetchMore : undefined}
|
||||
allowCustomValues={allowCustomValues}
|
||||
emptyOption={emptyOption}
|
||||
/>
|
||||
|
|
|
@ -41,17 +41,14 @@ export interface SingleAutocompleteSelectFieldProps
|
|||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
}
|
||||
|
||||
const DebounceAutocomplete: React.ComponentType<
|
||||
DebounceProps<string>
|
||||
> = Debounce;
|
||||
const DebounceAutocomplete: React.ComponentType<DebounceProps<
|
||||
string
|
||||
>> = Debounce;
|
||||
|
||||
const SingleAutocompleteSelectFieldComponent: React.FC<
|
||||
SingleAutocompleteSelectFieldProps
|
||||
> = props => {
|
||||
const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectFieldProps> = props => {
|
||||
const {
|
||||
choices,
|
||||
|
||||
allowCustomValues,
|
||||
choices,
|
||||
disabled,
|
||||
displayValue,
|
||||
emptyOption,
|
||||
|
@ -169,9 +166,11 @@ const SingleAutocompleteSelectFieldComponent: React.FC<
|
|||
);
|
||||
};
|
||||
|
||||
const SingleAutocompleteSelectField: React.FC<
|
||||
SingleAutocompleteSelectFieldProps
|
||||
> = ({ choices, fetchChoices, ...rest }) => {
|
||||
const SingleAutocompleteSelectField: React.FC<SingleAutocompleteSelectFieldProps> = ({
|
||||
choices,
|
||||
fetchChoices,
|
||||
...rest
|
||||
}) => {
|
||||
const [query, setQuery] = React.useState("");
|
||||
if (fetchChoices) {
|
||||
return (
|
||||
|
|
|
@ -8,6 +8,7 @@ import { makeStyles } from "@material-ui/core/styles";
|
|||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { InputProps } from "@material-ui/core/Input";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
|
@ -38,13 +39,13 @@ interface SingleSelectFieldProps {
|
|||
selectProps?: SelectProps;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
InputProps?: InputProps;
|
||||
onChange(event: any);
|
||||
}
|
||||
|
||||
export const SingleSelectField: React.FC<SingleSelectFieldProps> = props => {
|
||||
const {
|
||||
className,
|
||||
|
||||
disabled,
|
||||
error,
|
||||
label,
|
||||
|
@ -54,7 +55,8 @@ export const SingleSelectField: React.FC<SingleSelectFieldProps> = props => {
|
|||
name,
|
||||
hint,
|
||||
selectProps,
|
||||
placeholder
|
||||
placeholder,
|
||||
InputProps
|
||||
} = props;
|
||||
const classes = useStyles(props);
|
||||
|
||||
|
@ -90,6 +92,7 @@ export const SingleSelectField: React.FC<SingleSelectFieldProps> = props => {
|
|||
}}
|
||||
name={name}
|
||||
labelWidth={180}
|
||||
{...InputProps}
|
||||
/>
|
||||
}
|
||||
{...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 * from "./FilterTabs";
|
||||
export * from "./FilterTab";
|
||||
export * from "./FilterChips";
|
||||
|
|
|
@ -5,33 +5,41 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||
|
||||
import Container from "@saleor/components/Container";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import SearchBar from "@saleor/components/SearchBar";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import {
|
||||
ListActions,
|
||||
PageListProps,
|
||||
SearchPageProps,
|
||||
TabPageProps,
|
||||
SortPage
|
||||
SortPage,
|
||||
FilterPageProps
|
||||
} from "@saleor/types";
|
||||
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 { ListCustomers_customers_edges_node } from "../../types/ListCustomers";
|
||||
import {
|
||||
CustomerFilterKeys,
|
||||
CustomerListFilterOpts,
|
||||
createFilterStructure
|
||||
} from "./filters";
|
||||
|
||||
export interface CustomerListPageProps
|
||||
extends PageListProps,
|
||||
ListActions,
|
||||
SearchPageProps,
|
||||
FilterPageProps<CustomerFilterKeys, CustomerListFilterOpts>,
|
||||
SortPage<CustomerListUrlSortField>,
|
||||
TabPageProps {
|
||||
customers: ListCustomers_customers_edges_node[];
|
||||
}
|
||||
|
||||
const CustomerListPage: React.FC<CustomerListPageProps> = ({
|
||||
currencySymbol,
|
||||
currentTab,
|
||||
filterOpts,
|
||||
initialSearch,
|
||||
onAdd,
|
||||
onAll,
|
||||
onFilterChange,
|
||||
onSearchChange,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
|
@ -41,6 +49,8 @@ const CustomerListPage: React.FC<CustomerListPageProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const structure = createFilterStructure(intl, filterOpts);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<PageHeader title={intl.formatMessage(sectionNames.customers)}>
|
||||
|
@ -52,18 +62,21 @@ const CustomerListPage: React.FC<CustomerListPageProps> = ({
|
|||
</Button>
|
||||
</PageHeader>
|
||||
<Card>
|
||||
<SearchBar
|
||||
<FilterBar
|
||||
allTabLabel={intl.formatMessage({
|
||||
defaultMessage: "All Customers",
|
||||
description: "tab name"
|
||||
})}
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterStructure={structure}
|
||||
initialSearch={initialSearch}
|
||||
searchPlaceholder={intl.formatMessage({
|
||||
defaultMessage: "Search Customer"
|
||||
})}
|
||||
tabs={tabs}
|
||||
onAll={onAll}
|
||||
onFilterChange={onFilterChange}
|
||||
onSearchChange={onSearchChange}
|
||||
onTabChange={onTabChange}
|
||||
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 * from "./CustomerListPage";
|
||||
export * from "./filters";
|
||||
|
|
|
@ -16,6 +16,12 @@ export const customerSection = "/customers/";
|
|||
|
||||
export const customerListPath = customerSection;
|
||||
export enum CustomerListUrlFiltersEnum {
|
||||
joinedFrom = "joinedFrom",
|
||||
joinedTo = "joinedTo",
|
||||
moneySpentFrom = "moneySpentFrom",
|
||||
moneySpentTo = "moneySpentTo",
|
||||
numberOfOrdersFrom = "numberOfOrdersFrom",
|
||||
numberOfOrdersTo = "numberOfOrdersTo",
|
||||
query = "query"
|
||||
}
|
||||
export type CustomerListUrlFilters = Filters<CustomerListUrlFiltersEnum>;
|
||||
|
|
|
@ -22,6 +22,8 @@ import { ListViews } from "@saleor/types";
|
|||
import { getSortParams } from "@saleor/utils/sort";
|
||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||
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 { TypedBulkRemoveCustomers } from "../../mutations";
|
||||
import { useCustomerListQuery } from "../../queries";
|
||||
|
@ -29,7 +31,6 @@ import { BulkRemoveCustomers } from "../../types/BulkRemoveCustomers";
|
|||
import {
|
||||
customerAddUrl,
|
||||
customerListUrl,
|
||||
CustomerListUrlFilters,
|
||||
CustomerListUrlQueryParams,
|
||||
customerUrl,
|
||||
CustomerListUrlDialog
|
||||
|
@ -40,8 +41,10 @@ import {
|
|||
getActiveFilters,
|
||||
getFilterTabs,
|
||||
getFilterVariables,
|
||||
saveFilterTab
|
||||
} from "./filter";
|
||||
saveFilterTab,
|
||||
getFilterQueryParam,
|
||||
getFilterOpts
|
||||
} from "./filters";
|
||||
import { getSortQueryVariables } from "./sort";
|
||||
|
||||
interface CustomerListProps {
|
||||
|
@ -59,6 +62,7 @@ export const CustomerList: React.FC<CustomerListProps> = ({ params }) => {
|
|||
ListViews.CUSTOMER_LIST
|
||||
);
|
||||
const intl = useIntl();
|
||||
const shop = useShop();
|
||||
|
||||
const paginationState = createPaginationState(settings.rowNumber, params);
|
||||
const queryVariables = React.useMemo(
|
||||
|
@ -83,16 +87,17 @@ export const CustomerList: React.FC<CustomerListProps> = ({ params }) => {
|
|||
: 0
|
||||
: parseInt(params.activeTab, 0);
|
||||
|
||||
const changeFilterField = (filter: CustomerListUrlFilters) => {
|
||||
reset();
|
||||
navigate(
|
||||
customerListUrl({
|
||||
...getActiveFilters(params),
|
||||
...filter,
|
||||
activeTab: undefined
|
||||
})
|
||||
);
|
||||
};
|
||||
const [
|
||||
changeFilters,
|
||||
resetFilters,
|
||||
handleSearchChange
|
||||
] = createFilterHandlers({
|
||||
cleanupFn: reset,
|
||||
createUrl: customerListUrl,
|
||||
getFilterQueryParam,
|
||||
navigate,
|
||||
params
|
||||
});
|
||||
|
||||
const [openModal, closeModal] = createDialogActionHandlers<
|
||||
CustomerListUrlDialog,
|
||||
|
@ -138,16 +143,20 @@ export const CustomerList: React.FC<CustomerListProps> = ({ params }) => {
|
|||
};
|
||||
|
||||
const handleSort = createSortHandler(navigate, customerListUrl, params);
|
||||
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||
|
||||
return (
|
||||
<TypedBulkRemoveCustomers onCompleted={handleBulkCustomerDelete}>
|
||||
{(bulkRemoveCustomers, bulkRemoveCustomersOpts) => (
|
||||
<>
|
||||
<CustomerListPage
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
initialSearch={params.query || ""}
|
||||
onSearchChange={query => changeFilterField({ query })}
|
||||
onAll={() => navigate(customerListUrl())}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterChange={changeFilters}
|
||||
onAll={resetFilters}
|
||||
onTabChange={handleTabChange}
|
||||
onTabDelete={() => openModal("delete-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 PageHeader from "@saleor/components/PageHeader";
|
||||
import SearchBar from "@saleor/components/SearchBar";
|
||||
import FilterBar from "@saleor/components/FilterBar";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import {
|
||||
ListActions,
|
||||
PageListProps,
|
||||
SearchPageProps,
|
||||
TabPageProps,
|
||||
SortPage
|
||||
SortPage,
|
||||
FilterPageProps
|
||||
} from "@saleor/types";
|
||||
import { SaleListUrlSortField } from "@saleor/discounts/urls";
|
||||
import { SaleList_sales_edges_node } from "../../types/SaleList";
|
||||
import SaleList from "../SaleList";
|
||||
import {
|
||||
SaleFilterKeys,
|
||||
SaleListFilterOpts,
|
||||
createFilterStructure
|
||||
} from "./filters";
|
||||
|
||||
export interface SaleListPageProps
|
||||
extends PageListProps,
|
||||
ListActions,
|
||||
SearchPageProps,
|
||||
FilterPageProps<SaleFilterKeys, SaleListFilterOpts>,
|
||||
SortPage<SaleListUrlSortField>,
|
||||
TabPageProps {
|
||||
defaultCurrency: string;
|
||||
|
@ -29,10 +34,13 @@ export interface SaleListPageProps
|
|||
}
|
||||
|
||||
const SaleListPage: React.FC<SaleListPageProps> = ({
|
||||
currencySymbol,
|
||||
currentTab,
|
||||
filterOpts,
|
||||
initialSearch,
|
||||
onAdd,
|
||||
onAll,
|
||||
onFilterChange,
|
||||
onSearchChange,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
|
@ -42,6 +50,8 @@ const SaleListPage: React.FC<SaleListPageProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const structure = createFilterStructure(intl, filterOpts);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<PageHeader title={intl.formatMessage(sectionNames.sales)}>
|
||||
|
@ -50,18 +60,21 @@ const SaleListPage: React.FC<SaleListPageProps> = ({
|
|||
</Button>
|
||||
</PageHeader>
|
||||
<Card>
|
||||
<SearchBar
|
||||
<FilterBar
|
||||
allTabLabel={intl.formatMessage({
|
||||
defaultMessage: "All Sales",
|
||||
description: "tab name"
|
||||
})}
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterStructure={structure}
|
||||
initialSearch={initialSearch}
|
||||
searchPlaceholder={intl.formatMessage({
|
||||
defaultMessage: "Search Sale"
|
||||
})}
|
||||
tabs={tabs}
|
||||
onAll={onAll}
|
||||
onFilterChange={onFilterChange}
|
||||
onSearchChange={onSearchChange}
|
||||
onTabChange={onTabChange}
|
||||
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 * from "./SaleListPage";
|
||||
export * from "./filters";
|
||||
|
|
|
@ -5,23 +5,28 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||
|
||||
import Container from "@saleor/components/Container";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import SearchBar from "@saleor/components/SearchBar";
|
||||
import FilterBar from "@saleor/components/FilterBar";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import {
|
||||
ListActions,
|
||||
PageListProps,
|
||||
SearchPageProps,
|
||||
TabPageProps,
|
||||
SortPage
|
||||
SortPage,
|
||||
FilterPageProps
|
||||
} from "@saleor/types";
|
||||
import { VoucherListUrlSortField } from "@saleor/discounts/urls";
|
||||
import { VoucherList_vouchers_edges_node } from "../../types/VoucherList";
|
||||
import VoucherList from "../VoucherList";
|
||||
import {
|
||||
VoucherFilterKeys,
|
||||
VoucherListFilterOpts,
|
||||
createFilterStructure
|
||||
} from "./filters";
|
||||
|
||||
export interface VoucherListPageProps
|
||||
extends PageListProps,
|
||||
ListActions,
|
||||
SearchPageProps,
|
||||
FilterPageProps<VoucherFilterKeys, VoucherListFilterOpts>,
|
||||
SortPage<VoucherListUrlSortField>,
|
||||
TabPageProps {
|
||||
defaultCurrency: string;
|
||||
|
@ -29,10 +34,13 @@ export interface VoucherListPageProps
|
|||
}
|
||||
|
||||
const VoucherListPage: React.FC<VoucherListPageProps> = ({
|
||||
currencySymbol,
|
||||
currentTab,
|
||||
filterOpts,
|
||||
initialSearch,
|
||||
onAdd,
|
||||
onAll,
|
||||
onFilterChange,
|
||||
onSearchChange,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
|
@ -42,6 +50,8 @@ const VoucherListPage: React.FC<VoucherListPageProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const structure = createFilterStructure(intl, filterOpts);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<PageHeader title={intl.formatMessage(sectionNames.vouchers)}>
|
||||
|
@ -53,18 +63,21 @@ const VoucherListPage: React.FC<VoucherListPageProps> = ({
|
|||
</Button>
|
||||
</PageHeader>
|
||||
<Card>
|
||||
<SearchBar
|
||||
<FilterBar
|
||||
allTabLabel={intl.formatMessage({
|
||||
defaultMessage: "All Vouchers",
|
||||
description: "tab name"
|
||||
})}
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterStructure={structure}
|
||||
initialSearch={initialSearch}
|
||||
searchPlaceholder={intl.formatMessage({
|
||||
defaultMessage: "Search Voucher"
|
||||
})}
|
||||
tabs={tabs}
|
||||
onAll={onAll}
|
||||
onFilterChange={onFilterChange}
|
||||
onSearchChange={onSearchChange}
|
||||
onTabChange={onTabChange}
|
||||
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 * from "./VoucherListPage";
|
||||
export * from "./filters";
|
||||
|
|
|
@ -8,7 +8,8 @@ import {
|
|||
Filters,
|
||||
Pagination,
|
||||
TabActionDialog,
|
||||
Sort
|
||||
Sort,
|
||||
FiltersWithMultipleValues
|
||||
} from "../types";
|
||||
import { SaleDetailsPageTab } from "./components/SaleDetailsPage";
|
||||
import { VoucherDetailsPageTab } from "./components/VoucherDetailsPage";
|
||||
|
@ -18,9 +19,16 @@ export const discountSection = "/discounts/";
|
|||
export const saleSection = urlJoin(discountSection, "sales");
|
||||
export const saleListPath = saleSection;
|
||||
export enum SaleListUrlFiltersEnum {
|
||||
type = "type",
|
||||
startedFrom = "startedFrom",
|
||||
startedTo = "startedTo",
|
||||
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 enum SaleListUrlSortField {
|
||||
name = "name",
|
||||
|
@ -59,9 +67,18 @@ export const saleAddUrl = saleAddPath;
|
|||
export const voucherSection = urlJoin(discountSection, "vouchers");
|
||||
export const voucherListPath = voucherSection;
|
||||
export enum VoucherListUrlFiltersEnum {
|
||||
startedFrom = "startedFrom",
|
||||
startedTo = "startedTo",
|
||||
timesUsedFrom = "timesUsedFrom",
|
||||
timesUsedTo = "timesUsedTo",
|
||||
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 enum VoucherListUrlSortField {
|
||||
code = "code",
|
||||
|
|
|
@ -24,13 +24,13 @@ import { ListViews } from "@saleor/types";
|
|||
import { getSortParams } from "@saleor/utils/sort";
|
||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||
import SaleListPage from "../../components/SaleListPage";
|
||||
import { TypedSaleBulkDelete } from "../../mutations";
|
||||
import { useSaleListQuery } from "../../queries";
|
||||
import { SaleBulkDelete } from "../../types/SaleBulkDelete";
|
||||
import {
|
||||
saleAddUrl,
|
||||
SaleListUrlFilters,
|
||||
SaleListUrlQueryParams,
|
||||
saleUrl,
|
||||
saleListUrl,
|
||||
|
@ -42,8 +42,10 @@ import {
|
|||
getActiveFilters,
|
||||
getFilterTabs,
|
||||
getFilterVariables,
|
||||
saveFilterTab
|
||||
} from "./filter";
|
||||
saveFilterTab,
|
||||
getFilterQueryParam,
|
||||
getFilterOpts
|
||||
} from "./filters";
|
||||
import { getSortQueryVariables } from "./sort";
|
||||
|
||||
interface SaleListProps {
|
||||
|
@ -86,16 +88,17 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
|||
: 0
|
||||
: parseInt(params.activeTab, 0);
|
||||
|
||||
const changeFilterField = (filter: SaleListUrlFilters) => {
|
||||
reset();
|
||||
navigate(
|
||||
saleListUrl({
|
||||
...getActiveFilters(params),
|
||||
...filter,
|
||||
activeTab: undefined
|
||||
})
|
||||
);
|
||||
};
|
||||
const [
|
||||
changeFilters,
|
||||
resetFilters,
|
||||
handleSearchChange
|
||||
] = createFilterHandlers({
|
||||
cleanupFn: reset,
|
||||
createUrl: saleListUrl,
|
||||
getFilterQueryParam,
|
||||
navigate,
|
||||
params
|
||||
});
|
||||
|
||||
const [openModal, closeModal] = createDialogActionHandlers<
|
||||
SaleListUrlDialog,
|
||||
|
@ -143,6 +146,7 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
|||
};
|
||||
|
||||
const handleSort = createSortHandler(navigate, saleListUrl, params);
|
||||
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||
|
||||
return (
|
||||
<TypedSaleBulkDelete onCompleted={handleSaleBulkDelete}>
|
||||
|
@ -158,10 +162,13 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
|||
<>
|
||||
<WindowTitle title={intl.formatMessage(sectionNames.sales)} />
|
||||
<SaleListPage
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
initialSearch={params.query || ""}
|
||||
onSearchChange={query => changeFilterField({ query })}
|
||||
onAll={() => navigate(saleListUrl())}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterChange={filter => changeFilters(filter)}
|
||||
onAll={resetFilters}
|
||||
onTabChange={handleTabChange}
|
||||
onTabDelete={() => openModal("delete-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 createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||
import VoucherListPage from "../../components/VoucherListPage";
|
||||
import { TypedVoucherBulkDelete } from "../../mutations";
|
||||
import { useVoucherListQuery } from "../../queries";
|
||||
|
@ -31,7 +32,6 @@ import { VoucherBulkDelete } from "../../types/VoucherBulkDelete";
|
|||
import {
|
||||
voucherAddUrl,
|
||||
voucherListUrl,
|
||||
VoucherListUrlFilters,
|
||||
VoucherListUrlQueryParams,
|
||||
voucherUrl,
|
||||
VoucherListUrlDialog
|
||||
|
@ -42,8 +42,10 @@ import {
|
|||
getActiveFilters,
|
||||
getFilterTabs,
|
||||
getFilterVariables,
|
||||
saveFilterTab
|
||||
} from "./filter";
|
||||
saveFilterTab,
|
||||
getFilterQueryParam,
|
||||
getFilterOpts
|
||||
} from "./filters";
|
||||
import { getSortQueryVariables } from "./sort";
|
||||
|
||||
interface VoucherListProps {
|
||||
|
@ -86,16 +88,17 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
|
|||
: 0
|
||||
: parseInt(params.activeTab, 0);
|
||||
|
||||
const changeFilterField = (filter: VoucherListUrlFilters) => {
|
||||
reset();
|
||||
navigate(
|
||||
voucherListUrl({
|
||||
...getActiveFilters(params),
|
||||
...filter,
|
||||
activeTab: undefined
|
||||
})
|
||||
);
|
||||
};
|
||||
const [
|
||||
changeFilters,
|
||||
resetFilters,
|
||||
handleSearchChange
|
||||
] = createFilterHandlers({
|
||||
cleanupFn: reset,
|
||||
createUrl: voucherListUrl,
|
||||
getFilterQueryParam,
|
||||
navigate,
|
||||
params
|
||||
});
|
||||
|
||||
const [openModal, closeModal] = createDialogActionHandlers<
|
||||
VoucherListUrlDialog,
|
||||
|
@ -143,6 +146,7 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
|
|||
};
|
||||
|
||||
const handleSort = createSortHandler(navigate, voucherListUrl, params);
|
||||
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||
|
||||
return (
|
||||
<TypedVoucherBulkDelete onCompleted={handleVoucherBulkDelete}>
|
||||
|
@ -158,10 +162,13 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
|
|||
<>
|
||||
<WindowTitle title={intl.formatMessage(sectionNames.vouchers)} />
|
||||
<VoucherListPage
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
initialSearch={params.query || ""}
|
||||
onSearchChange={query => changeFilterField({ query })}
|
||||
onAll={() => navigate(voucherListUrl())}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterChange={filter => changeFilters(filter)}
|
||||
onAll={resetFilters}
|
||||
onTabChange={handleTabChange}
|
||||
onTabDelete={() => openModal("delete-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 { Filter } from "./components/TableFilter";
|
||||
import {
|
||||
FetchMoreProps,
|
||||
FilterPageProps,
|
||||
|
@ -303,80 +302,14 @@ export const searchPageProps: SearchPageProps = {
|
|||
onSearchChange: () => undefined
|
||||
};
|
||||
|
||||
export const filterPageProps: FilterPageProps = {
|
||||
export const filterPageProps: FilterPageProps<string, object> = {
|
||||
...searchPageProps,
|
||||
...tabPageProps,
|
||||
currencySymbol: "USD",
|
||||
filtersList: [],
|
||||
onFilterAdd: () => undefined
|
||||
filterOpts: {},
|
||||
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 = {
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
|
@ -443,3 +376,8 @@ export const permissions: ShopInfo_shop_permissions[] = [
|
|||
__typename: "PermissionDisplay" as "PermissionDisplay",
|
||||
...perm
|
||||
}));
|
||||
|
||||
export const date = {
|
||||
from: "2019-12-09",
|
||||
to: "2019-12-38"
|
||||
};
|
||||
|
|
|
@ -48,7 +48,7 @@ const HomeSection = () => {
|
|||
onProductsOutOfStockClick={() =>
|
||||
navigate(
|
||||
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: {
|
||||
defaultMessage: "Start Hour"
|
||||
},
|
||||
status: {
|
||||
defaultMessage: "Status"
|
||||
},
|
||||
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: {
|
||||
defaultMessage: "Cancelled",
|
||||
description: "order status"
|
||||
|
@ -351,14 +351,14 @@ export function findInEnum<TEnum extends object>(
|
|||
export function findValueInEnum<TEnum extends object>(
|
||||
needle: string,
|
||||
haystack: TEnum
|
||||
) {
|
||||
): TEnum[keyof TEnum] {
|
||||
const match = Object.entries(haystack).find(([_, value]) => value === needle);
|
||||
|
||||
if (!!match) {
|
||||
return match[1] as TEnum;
|
||||
if (!match) {
|
||||
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 {
|
||||
|
|
|
@ -6,34 +6,42 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||
|
||||
import Container from "@saleor/components/Container";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import SearchBar from "@saleor/components/SearchBar";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import {
|
||||
ListActions,
|
||||
PageListProps,
|
||||
SearchPageProps,
|
||||
TabPageProps,
|
||||
SortPage
|
||||
SortPage,
|
||||
FilterPageProps
|
||||
} from "@saleor/types";
|
||||
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_draftOrders_edges_node } from "../../types/OrderDraftList";
|
||||
import {
|
||||
OrderDraftListFilterOpts,
|
||||
OrderDraftFilterKeys,
|
||||
createFilterStructure
|
||||
} from "./filters";
|
||||
|
||||
export interface OrderDraftListPageProps
|
||||
extends PageListProps,
|
||||
ListActions,
|
||||
SearchPageProps,
|
||||
FilterPageProps<OrderDraftFilterKeys, OrderDraftListFilterOpts>,
|
||||
SortPage<OrderDraftListUrlSortField>,
|
||||
TabPageProps {
|
||||
orders: OrderDraftList_draftOrders_edges_node[];
|
||||
}
|
||||
|
||||
const OrderDraftListPage: React.FC<OrderDraftListPageProps> = ({
|
||||
currencySymbol,
|
||||
currentTab,
|
||||
disabled,
|
||||
filterOpts,
|
||||
initialSearch,
|
||||
onAdd,
|
||||
onAll,
|
||||
onFilterChange,
|
||||
onSearchChange,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
|
@ -43,6 +51,8 @@ const OrderDraftListPage: React.FC<OrderDraftListPageProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const structure = createFilterStructure(intl, filterOpts);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<PageHeader title={intl.formatMessage(sectionNames.draftOrders)}>
|
||||
|
@ -59,18 +69,21 @@ const OrderDraftListPage: React.FC<OrderDraftListPageProps> = ({
|
|||
</Button>
|
||||
</PageHeader>
|
||||
<Card>
|
||||
<SearchBar
|
||||
<FilterBar
|
||||
allTabLabel={intl.formatMessage({
|
||||
defaultMessage: "All Drafts",
|
||||
description: "tab name"
|
||||
})}
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterStructure={structure}
|
||||
initialSearch={initialSearch}
|
||||
searchPlaceholder={intl.formatMessage({
|
||||
defaultMessage: "Search Draft"
|
||||
})}
|
||||
tabs={tabs}
|
||||
onAll={onAll}
|
||||
onFilterChange={onFilterChange}
|
||||
onSearchChange={onSearchChange}
|
||||
onTabChange={onTabChange}
|
||||
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 * 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
|
||||
} from "@saleor/types";
|
||||
import { OrderListUrlSortField } from "@saleor/orders/urls";
|
||||
import FilterBar from "@saleor/components/FilterBar";
|
||||
import { OrderList_orders_edges_node } from "../../types/OrderList";
|
||||
import OrderList from "../OrderList";
|
||||
import OrderListFilter, { OrderFilterKeys } from "../OrderListFilter";
|
||||
import {
|
||||
createFilterStructure,
|
||||
OrderListFilterOpts,
|
||||
OrderFilterKeys
|
||||
} from "./filters";
|
||||
|
||||
export interface OrderListPageProps
|
||||
extends PageListProps,
|
||||
ListActions,
|
||||
FilterPageProps<OrderFilterKeys>,
|
||||
FilterPageProps<OrderFilterKeys, OrderListFilterOpts>,
|
||||
SortPage<OrderListUrlSortField> {
|
||||
orders: OrderList_orders_edges_node[];
|
||||
}
|
||||
|
@ -29,13 +34,13 @@ export interface OrderListPageProps
|
|||
const OrderListPage: React.FC<OrderListPageProps> = ({
|
||||
currencySymbol,
|
||||
currentTab,
|
||||
filtersList,
|
||||
initialSearch,
|
||||
filterOpts,
|
||||
tabs,
|
||||
onAdd,
|
||||
onAll,
|
||||
onSearchChange,
|
||||
onFilterAdd,
|
||||
onFilterChange,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
onTabSave,
|
||||
|
@ -43,6 +48,8 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const filterStructure = createFilterStructure(intl, filterOpts);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<PageHeader title={intl.formatMessage(sectionNames.orders)}>
|
||||
|
@ -54,28 +61,25 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
|
|||
</Button>
|
||||
</PageHeader>
|
||||
<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({
|
||||
defaultMessage: "All Orders",
|
||||
description: "tab name"
|
||||
})}
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterLabel={intl.formatMessage({
|
||||
defaultMessage: "Select all orders where:"
|
||||
})}
|
||||
tabs={tabs}
|
||||
filtersList={filtersList}
|
||||
initialSearch={initialSearch}
|
||||
filterStructure={filterStructure}
|
||||
searchPlaceholder={intl.formatMessage({
|
||||
defaultMessage: "Search Orders..."
|
||||
})}
|
||||
onAll={onAll}
|
||||
onSearchChange={onSearchChange}
|
||||
onFilterAdd={onFilterAdd}
|
||||
onTabChange={onTabChange}
|
||||
onTabDelete={onTabDelete}
|
||||
onTabSave={onTabSave}
|
||||
/>
|
||||
<OrderList {...listProps} />
|
||||
</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 * from "./OrderListPage";
|
||||
export * from "./filters";
|
||||
|
|
|
@ -17,8 +17,8 @@ const orderSectionUrl = "/orders";
|
|||
|
||||
export const orderListPath = orderSectionUrl;
|
||||
export enum OrderListUrlFiltersEnum {
|
||||
dateFrom = "dateFrom",
|
||||
dateTo = "dateTo",
|
||||
createdFrom = "createdFrom",
|
||||
createdTo = "createdTo",
|
||||
email = "email",
|
||||
payment = "payment",
|
||||
query = "query"
|
||||
|
@ -55,6 +55,9 @@ export const orderListUrl = (params?: OrderListUrlQueryParams): string => {
|
|||
|
||||
export const orderDraftListPath = urlJoin(orderSectionUrl, "drafts");
|
||||
export enum OrderDraftListUrlFiltersEnum {
|
||||
createdFrom = "createdFrom",
|
||||
createdTo = "createdTo",
|
||||
customer = "customer",
|
||||
query = "query"
|
||||
}
|
||||
export type OrderDraftListUrlFilters = Filters<OrderDraftListUrlFiltersEnum>;
|
||||
|
|
|
@ -21,6 +21,8 @@ import { ListViews } from "@saleor/types";
|
|||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||
import { getSortParams } from "@saleor/utils/sort";
|
||||
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 {
|
||||
TypedOrderDraftBulkCancelMutation,
|
||||
|
@ -31,7 +33,6 @@ import { OrderDraftBulkCancel } from "../../types/OrderDraftBulkCancel";
|
|||
import { OrderDraftCreate } from "../../types/OrderDraftCreate";
|
||||
import {
|
||||
orderDraftListUrl,
|
||||
OrderDraftListUrlFilters,
|
||||
OrderDraftListUrlQueryParams,
|
||||
orderUrl,
|
||||
OrderDraftListUrlDialog
|
||||
|
@ -42,8 +43,10 @@ import {
|
|||
getActiveFilters,
|
||||
getFilterTabs,
|
||||
getFilterVariables,
|
||||
saveFilterTab
|
||||
} from "./filter";
|
||||
saveFilterTab,
|
||||
getFilterQueryParam,
|
||||
getFilterOpts
|
||||
} from "./filters";
|
||||
import { getSortQueryVariables } from "./sort";
|
||||
|
||||
interface OrderDraftListProps {
|
||||
|
@ -61,6 +64,7 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
|
|||
ListViews.DRAFT_LIST
|
||||
);
|
||||
const intl = useIntl();
|
||||
const shop = useShop();
|
||||
|
||||
const handleCreateOrderCreateSuccess = (data: OrderDraftCreate) => {
|
||||
notify({
|
||||
|
@ -84,16 +88,17 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
|
|||
: 0
|
||||
: parseInt(params.activeTab, 0);
|
||||
|
||||
const changeFilterField = (filter: OrderDraftListUrlFilters) => {
|
||||
reset();
|
||||
navigate(
|
||||
orderDraftListUrl({
|
||||
...getActiveFilters(params),
|
||||
...filter,
|
||||
activeTab: undefined
|
||||
})
|
||||
);
|
||||
};
|
||||
const [
|
||||
changeFilters,
|
||||
resetFilters,
|
||||
handleSearchChange
|
||||
] = createFilterHandlers({
|
||||
cleanupFn: reset,
|
||||
createUrl: orderDraftListUrl,
|
||||
getFilterQueryParam,
|
||||
navigate,
|
||||
params
|
||||
});
|
||||
|
||||
const [openModal, closeModal] = createDialogActionHandlers<
|
||||
OrderDraftListUrlDialog,
|
||||
|
@ -155,6 +160,7 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
|
|||
};
|
||||
|
||||
const handleSort = createSortHandler(navigate, orderDraftListUrl, params);
|
||||
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
|
||||
|
||||
return (
|
||||
<TypedOrderDraftBulkCancelMutation onCompleted={handleOrderDraftBulkCancel}>
|
||||
|
@ -169,10 +175,13 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
|
|||
return (
|
||||
<>
|
||||
<OrderDraftListPage
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
initialSearch={params.query || ""}
|
||||
onSearchChange={query => changeFilterField({ query })}
|
||||
onAll={() => navigate(orderDraftListUrl())}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterChange={changeFilters}
|
||||
onAll={resetFilters}
|
||||
onTabChange={handleTabChange}
|
||||
onTabDelete={() => openModal("delete-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
|
||||
} from "@saleor/components/SaveFilterTabDialog";
|
||||
import useBulkActions from "@saleor/hooks/useBulkActions";
|
||||
import useDateLocalize from "@saleor/hooks/useDateLocalize";
|
||||
import useListSettings from "@saleor/hooks/useListSettings";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
|
@ -20,6 +19,7 @@ import { ListViews } from "@saleor/types";
|
|||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||
import { getSortParams } from "@saleor/utils/sort";
|
||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||
import OrderBulkCancelDialog from "../../components/OrderBulkCancelDialog";
|
||||
import OrderListPage from "../../components/OrderListPage/OrderListPage";
|
||||
import {
|
||||
|
@ -31,20 +31,19 @@ import { OrderBulkCancel } from "../../types/OrderBulkCancel";
|
|||
import { OrderDraftCreate } from "../../types/OrderDraftCreate";
|
||||
import {
|
||||
orderListUrl,
|
||||
OrderListUrlFilters,
|
||||
OrderListUrlQueryParams,
|
||||
orderUrl,
|
||||
OrderListUrlDialog
|
||||
} from "../../urls";
|
||||
import {
|
||||
areFiltersApplied,
|
||||
createFilter,
|
||||
createFilterChips,
|
||||
deleteFilterTab,
|
||||
getActiveFilters,
|
||||
getFilterTabs,
|
||||
getFilterOpts,
|
||||
getFilterVariables,
|
||||
saveFilterTab
|
||||
saveFilterTab,
|
||||
getFilterQueryParam
|
||||
} from "./filters";
|
||||
import { getSortQueryVariables } from "./sort";
|
||||
|
||||
|
@ -53,7 +52,6 @@ interface OrderListProps {
|
|||
}
|
||||
|
||||
export const OrderList: React.FC<OrderListProps> = ({ params }) => {
|
||||
const formatDate = useDateLocalize();
|
||||
const navigate = useNavigator();
|
||||
const notify = useNotifier();
|
||||
const paginate = usePaginator();
|
||||
|
@ -88,21 +86,17 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
|
|||
: 0
|
||||
: parseInt(params.activeTab, 0);
|
||||
|
||||
const changeFilters = (filters: OrderListUrlFilters) => {
|
||||
reset();
|
||||
navigate(orderListUrl(filters));
|
||||
};
|
||||
|
||||
const changeFilterField = (filter: OrderListUrlFilters) => {
|
||||
reset();
|
||||
navigate(
|
||||
orderListUrl({
|
||||
...getActiveFilters(params),
|
||||
...filter,
|
||||
activeTab: undefined
|
||||
})
|
||||
);
|
||||
};
|
||||
const [
|
||||
changeFilters,
|
||||
resetFilters,
|
||||
handleSearchChange
|
||||
] = createFilterHandlers({
|
||||
cleanupFn: reset,
|
||||
createUrl: orderListUrl,
|
||||
getFilterQueryParam,
|
||||
navigate,
|
||||
params
|
||||
});
|
||||
|
||||
const [openModal, closeModal] = createDialogActionHandlers<
|
||||
OrderListUrlDialog,
|
||||
|
@ -183,16 +177,9 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
|
|||
<OrderListPage
|
||||
currencySymbol={currencySymbol}
|
||||
settings={settings}
|
||||
filtersList={createFilterChips(
|
||||
params,
|
||||
{
|
||||
formatDate
|
||||
},
|
||||
changeFilterField,
|
||||
intl
|
||||
)}
|
||||
currentTab={currentTab}
|
||||
disabled={loading}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
orders={maybe(() => data.orders.edges.map(edge => edge.node))}
|
||||
pageInfo={pageInfo}
|
||||
sort={getSortParams(params)}
|
||||
|
@ -221,20 +208,14 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
|
|||
/>
|
||||
</Button>
|
||||
}
|
||||
onSearchChange={query => changeFilterField({ query })}
|
||||
onFilterAdd={data =>
|
||||
changeFilterField(createFilter(params, data))
|
||||
}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterChange={changeFilters}
|
||||
onTabSave={() => openModal("save-search")}
|
||||
onTabDelete={() => openModal("delete-search")}
|
||||
onTabChange={handleTabChange}
|
||||
initialSearch={params.query || ""}
|
||||
tabs={getFilterTabs().map(tab => tab.name)}
|
||||
onAll={() =>
|
||||
changeFilters({
|
||||
status: undefined
|
||||
})
|
||||
}
|
||||
onAll={resetFilters}
|
||||
/>
|
||||
<OrderBulkCancelDialog
|
||||
confirmButtonState={orderBulkCancelOpts.status}
|
||||
|
|
|
@ -1,83 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Crate filter chips 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`] = `
|
||||
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
|
||||
Object {
|
||||
"createdFrom": "2019-12-09",
|
||||
"createdTo": "2019-12-38",
|
||||
"status": Array [
|
||||
"FULFILLED",
|
||||
"PARTIALLY_FULFILLED",
|
||||
|
@ -85,31 +11,4 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Get filter variables from multiple status value 1`] = `
|
||||
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",
|
||||
],
|
||||
}
|
||||
`;
|
||||
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"`;
|
||||
|
|
|
@ -1,158 +1,75 @@
|
|||
import { createIntl } from "react-intl";
|
||||
import { stringify as stringifyQs } from "qs";
|
||||
|
||||
import { OrderFilterKeys } from "@saleor/orders/components/OrderListFilter";
|
||||
import { OrderStatus, OrderStatusFilter } from "@saleor/types/globalTypes";
|
||||
import { createFilter, createFilterChips, getFilterVariables } from "./filters";
|
||||
import { OrderListUrlFilters } from "@saleor/orders/urls";
|
||||
import { createFilterStructure } from "@saleor/orders/components/OrderListPage";
|
||||
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({
|
||||
locale: "en"
|
||||
});
|
||||
describe("Filtering query params", () => {
|
||||
it("should be empty object if no params given", () => {
|
||||
const params: OrderListUrlFilters = {};
|
||||
const filterVariables = getFilterVariables(params);
|
||||
|
||||
describe("Create filter object", () => {
|
||||
it("with date", () => {
|
||||
const filter = createFilter(
|
||||
{},
|
||||
{
|
||||
name: OrderFilterKeys.dateEqual,
|
||||
value: "2019-09-01"
|
||||
}
|
||||
);
|
||||
|
||||
expect(filter).toMatchSnapshot();
|
||||
expect(getExistingKeys(filterVariables)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("with date range", () => {
|
||||
const filter = createFilter(
|
||||
{},
|
||||
{
|
||||
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",
|
||||
it("should not be empty object if params given", () => {
|
||||
const params: OrderListUrlFilters = {
|
||||
createdFrom: date.from,
|
||||
createdTo: date.to,
|
||||
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: [
|
||||
OrderStatus.FULFILLED.toString(),
|
||||
OrderStatus.PARTIALLY_FULFILLED.toString()
|
||||
OrderStatusFilter.FULFILLED,
|
||||
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 } from "@saleor/misc";
|
||||
import { removeAtIndex } from "@saleor/utils/lists";
|
||||
import { FilterContentSubmitData } from "../../../components/Filter";
|
||||
import { Filter } from "../../../components/TableFilter";
|
||||
import { findInEnum, maybe, findValueInEnum } from "@saleor/misc";
|
||||
import {
|
||||
OrderListFilterOpts,
|
||||
OrderFilterKeys
|
||||
} from "@saleor/orders/components/OrderListPage/filters";
|
||||
import { IFilterElement } from "../../../components/Filter";
|
||||
import {
|
||||
OrderFilterInput,
|
||||
OrderStatusFilter
|
||||
OrderStatusFilter,
|
||||
OrderStatus
|
||||
} from "../../../types/globalTypes";
|
||||
import {
|
||||
arrayOrUndefined,
|
||||
arrayOrValue,
|
||||
createFilterTabUtils,
|
||||
createFilterUtils,
|
||||
dedupeFilter,
|
||||
valueOrFirst
|
||||
getGteLteVariables,
|
||||
getMinMaxQueryParam,
|
||||
getMultipleEnumValueQueryParam
|
||||
} from "../../../utils/filters";
|
||||
import { OrderFilterKeys } from "../../components/OrderListFilter";
|
||||
import {
|
||||
OrderListUrlFilters,
|
||||
OrderListUrlFiltersEnum,
|
||||
|
@ -26,213 +26,74 @@ import {
|
|||
|
||||
export const ORDER_FILTERS_KEY = "orderFilters";
|
||||
|
||||
const filterMessages = defineMessages({
|
||||
dateFrom: {
|
||||
defaultMessage: "Date from {date}",
|
||||
description: "filter by date"
|
||||
},
|
||||
dateIs: {
|
||||
defaultMessage: "Date is {date}",
|
||||
description: "filter by date"
|
||||
},
|
||||
dateTo: {
|
||||
defaultMessage: "Date to {date}",
|
||||
description: "filter by date"
|
||||
},
|
||||
fulfilled: {
|
||||
defaultMessage: "Fulfilled",
|
||||
description: "order status"
|
||||
},
|
||||
partiallyFulfilled: {
|
||||
defaultMessage: "Partially Fulfilled",
|
||||
description: "order status"
|
||||
},
|
||||
readyToCapture: {
|
||||
defaultMessage: "Ready to Capture",
|
||||
description: "order status"
|
||||
},
|
||||
unfulfilled: {
|
||||
defaultMessage: "Unfulfilled",
|
||||
description: "order status"
|
||||
export function getFilterOpts(
|
||||
params: OrderListUrlFilters
|
||||
): OrderListFilterOpts {
|
||||
return {
|
||||
created: {
|
||||
active: maybe(
|
||||
() =>
|
||||
[params.createdFrom, params.createdTo].some(
|
||||
field => field !== undefined
|
||||
),
|
||||
false
|
||||
),
|
||||
value: {
|
||||
max: maybe(() => params.createdTo, ""),
|
||||
min: maybe(() => params.createdFrom, "")
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
},
|
||||
status: {
|
||||
active: maybe(() => params.status !== undefined, false),
|
||||
value: maybe(
|
||||
() =>
|
||||
dedupeFilter(
|
||||
params.status.map(status =>
|
||||
findValueInEnum(status, OrderStatusFilter)
|
||||
)
|
||||
),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
}
|
||||
|
||||
export function getFilterVariables(
|
||||
params: OrderListUrlFilters
|
||||
): OrderFilterInput {
|
||||
return {
|
||||
created: {
|
||||
gte: params.dateFrom,
|
||||
lte: params.dateTo
|
||||
},
|
||||
created: getGteLteVariables({
|
||||
gte: params.createdFrom,
|
||||
lte: params.createdTo
|
||||
}),
|
||||
customer: params.email,
|
||||
search: params.query,
|
||||
status: Array.isArray(params.status)
|
||||
? params.status.map(status => findInEnum(status, OrderStatusFilter))
|
||||
: params.status
|
||||
? [findInEnum(params.status, OrderStatusFilter)]
|
||||
: undefined
|
||||
status: maybe(() =>
|
||||
params.status.map(status => findInEnum(status, OrderStatusFilter))
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
export function createFilter(
|
||||
filter: OrderListUrlFilters,
|
||||
data: FilterContentSubmitData<OrderFilterKeys>
|
||||
export function getFilterQueryParam(
|
||||
filter: IFilterElement<OrderFilterKeys>
|
||||
): OrderListUrlFilters {
|
||||
const { name: filterName, value } = data;
|
||||
if (filterName === OrderFilterKeys.dateEqual) {
|
||||
return {
|
||||
dateFrom: valueOrFirst(value),
|
||||
dateTo: valueOrFirst(value)
|
||||
};
|
||||
} else if (filterName === OrderFilterKeys.dateRange) {
|
||||
return {
|
||||
dateFrom: value[0],
|
||||
dateTo: value[1]
|
||||
};
|
||||
} else if (
|
||||
[
|
||||
OrderFilterKeys.dateLastWeek,
|
||||
OrderFilterKeys.dateLastMonth,
|
||||
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)
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
const { name } = filter;
|
||||
|
||||
interface OrderListChipFormatData {
|
||||
formatDate: (date: string) => string;
|
||||
}
|
||||
export function createFilterChips(
|
||||
filters: OrderListUrlFilters,
|
||||
formatData: OrderListChipFormatData,
|
||||
onFilterDelete: (filters: OrderListUrlFilters) => void,
|
||||
intl: IntlShape
|
||||
): Filter[] {
|
||||
let filterChips: Filter[] = [];
|
||||
switch (name) {
|
||||
case OrderFilterKeys.created:
|
||||
return getMinMaxQueryParam(
|
||||
filter,
|
||||
OrderListUrlFiltersEnum.createdFrom,
|
||||
OrderListUrlFiltersEnum.createdTo
|
||||
);
|
||||
|
||||
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
|
||||
})
|
||||
case OrderFilterKeys.status:
|
||||
return getMultipleEnumValueQueryParam(
|
||||
filter,
|
||||
OrderListUrlFiltersWithMultipleValuesEnum.status,
|
||||
OrderStatus
|
||||
);
|
||||
}
|
||||
];
|
||||
} 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 {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import Card from "@material-ui/core/Card";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import TableBody from "@material-ui/core/TableBody";
|
||||
import TableCell from "@material-ui/core/TableCell";
|
||||
|
@ -65,10 +64,9 @@ const PluginList: React.FC<PluginListProps> = props => {
|
|||
onPreviousPage
|
||||
} = props;
|
||||
const classes = useStyles(props);
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<ResponsiveTable>
|
||||
<TableHead>
|
||||
<TableCellHeader
|
||||
|
@ -165,7 +163,6 @@ const PluginList: React.FC<PluginListProps> = props => {
|
|||
)}
|
||||
</TableBody>
|
||||
</ResponsiveTable>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
PluginList.displayName = "PluginList";
|
||||
|
|
|
@ -1,35 +1,85 @@
|
|||
import React from "react";
|
||||
import Card from "@material-ui/core/Card";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import AppHeader from "@saleor/components/AppHeader";
|
||||
import Container from "@saleor/components/Container";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
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 FilterBar from "@saleor/components/FilterBar";
|
||||
import { Plugins_plugins_edges_node } from "../../types/Plugins";
|
||||
import PluginsList from "../PluginsList/PluginsList";
|
||||
import {
|
||||
createFilterStructure,
|
||||
PluginFilterKeys,
|
||||
PluginListFilterOpts
|
||||
} from "./filters";
|
||||
|
||||
export interface PluginsListPageProps
|
||||
extends PageListProps,
|
||||
SortPage<PluginListUrlSortField> {
|
||||
FilterPageProps<PluginFilterKeys, PluginListFilterOpts>,
|
||||
SortPage<PluginListUrlSortField>,
|
||||
TabPageProps {
|
||||
plugins: Plugins_plugins_edges_node[];
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const PluginsListPage: React.FC<PluginsListPageProps> = ({
|
||||
currencySymbol,
|
||||
currentTab,
|
||||
initialSearch,
|
||||
filterOpts,
|
||||
tabs,
|
||||
onAdd,
|
||||
onAll,
|
||||
onBack,
|
||||
onSearchChange,
|
||||
onFilterChange,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
onTabSave,
|
||||
...listProps
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const filterStructure = createFilterStructure(intl, filterOpts);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<AppHeader onBack={onBack}>
|
||||
{intl.formatMessage(sectionNames.configuration)}
|
||||
</AppHeader>
|
||||
<PageHeader title={intl.formatMessage(sectionNames.plugins)} />
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
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 * from "./PluginsListPage";
|
||||
export * from "./filters";
|
||||
|
|
|
@ -35,6 +35,7 @@ const pluginsList = gql`
|
|||
$after: String
|
||||
$last: Int
|
||||
$before: String
|
||||
$filter: PluginFilterInput
|
||||
$sort: PluginSortingInput
|
||||
) {
|
||||
plugins(
|
||||
|
@ -42,6 +43,7 @@ const pluginsList = gql`
|
|||
after: $after
|
||||
first: $first
|
||||
last: $last
|
||||
filter: $filter
|
||||
sortBy: $sort
|
||||
) {
|
||||
edges {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/* eslint-disable */
|
||||
// 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
|
||||
|
@ -44,5 +44,6 @@ export interface PluginsVariables {
|
|||
after?: string | null;
|
||||
last?: number | null;
|
||||
before?: string | null;
|
||||
filter?: PluginFilterInput | 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