Merge branch 'master' into fix/password-reset

This commit is contained in:
Marcin Gębala 2020-01-20 17:18:54 +01:00 committed by GitHub
commit 95df0e8739
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
186 changed files with 8767 additions and 7936 deletions

View file

@ -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

View file

@ -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
View file

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

View file

@ -66,7 +66,7 @@ input AccountInput {
type AccountRegister {
errors: [Error!]
requiresConfirmation: Boolean!
requiresConfirmation: Boolean
accountErrors: [AccountError!]
user: User
}

View file

@ -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}

View 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
}
];
}

View file

@ -1,2 +1,3 @@
export { default } from "./AttributeListPage";
export * from "./AttributeListPage";
export * from "./filters";

View file

@ -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>;

View file

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

View file

@ -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"`;

View 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();
});
});

View file

@ -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,

View file

@ -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}

View 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
}
];
}

View file

@ -1,2 +1,3 @@
export { default } from "./CollectionListPage";
export * from "./CollectionListPage";
export * from "./filters";

View file

@ -15,6 +15,7 @@ const collectionSectionUrl = "/collections/";
export const collectionListPath = collectionSectionUrl;
export enum CollectionListUrlFiltersEnum {
status = "status",
query = "query"
}
export type CollectionListUrlFilters = Filters<CollectionListUrlFiltersEnum>;

View file

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

View file

@ -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"`;

View file

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

View 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();
});
});

View 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);

View file

@ -47,7 +47,6 @@ const useStyles = makeStyles(
},
bottom: 0,
gridColumn: 2,
height: 70,
position: "sticky",
zIndex: 10
},

View 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;

View file

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

View file

@ -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;

View 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;

View file

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

View file

@ -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;

View 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;

View file

@ -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;

View 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;

View file

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

View 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;

View file

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

View file

@ -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

View file

@ -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}
/>
)}

View file

@ -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: {

View file

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

View 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;

View file

@ -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}
/>

View file

@ -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 (

View file

@ -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}

View file

@ -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;

View file

@ -1,4 +1,3 @@
export { default } from "./FilterTabs";
export * from "./FilterTabs";
export * from "./FilterTab";
export * from "./FilterChips";

View file

@ -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}

View 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
}
];
}

View file

@ -1,2 +1,3 @@
export { default } from "./CustomerListPage";
export * from "./CustomerListPage";
export * from "./filters";

View file

@ -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>;

View file

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

View file

@ -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"`;

View file

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

View 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();
});
});

View 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);

View file

@ -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}

View 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
}
];
}

View file

@ -1,2 +1,3 @@
export { default } from "./SaleListPage";
export * from "./SaleListPage";
export * from "./filters";

View file

@ -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}

View 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
}
];
}

View file

@ -1,2 +1,3 @@
export { default } from "./VoucherListPage";
export * from "./VoucherListPage";
export * from "./filters";

View file

@ -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",

View file

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

View file

@ -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"`;

View file

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

View 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();
});
});

View 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);

View file

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

View file

@ -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&timesUsedFrom=1&timesUsedTo=6&status%5B0%5D=ACTIVE&status%5B1%5D=EXPIRED&type%5B0%5D=FIXED&type%5B1%5D=SHIPPING"`;

View file

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

View 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();
});
});

View 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);

View file

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

View file

@ -48,7 +48,7 @@ const HomeSection = () => {
onProductsOutOfStockClick={() =>
navigate(
productListUrl({
status: StockAvailability.OUT_OF_STOCK
stockStatus: StockAvailability.OUT_OF_STOCK
})
)
}

View 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;

View file

@ -65,6 +65,9 @@ export const commonMessages = defineMessages({
startHour: {
defaultMessage: "Start Hour"
},
status: {
defaultMessage: "Status"
},
summary: {
defaultMessage: "Summary"
},

View file

@ -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 {

View file

@ -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}

View 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
}
];
}

View file

@ -1,2 +1,3 @@
export { default } from "./OrderDraftListPage";
export * from "./OrderDraftListPage";
export * from "./filters";

View file

@ -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;

View file

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

View file

@ -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>

View 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
}
];
}

View file

@ -1,2 +1,3 @@
export { default } from "./OrderListPage";
export * from "./OrderListPage";
export * from "./filters";

View file

@ -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>;

View file

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

View file

@ -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"`;

View file

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

View 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();
});
});

View 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);

View file

@ -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}

View file

@ -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"`;

View file

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

View file

@ -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 {

View file

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

View file

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

View 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
}
];
}

View file

@ -1,2 +1,3 @@
export { default } from "./PluginsListPage";
export * from "./PluginsListPage";
export * from "./filters";

View file

@ -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 {

View file

@ -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