Add tabs and filters to plugins

This commit is contained in:
dominik-zeglen 2020-01-10 17:30:42 +01:00
parent e4397b1f75
commit 35324951ec
10 changed files with 388 additions and 106 deletions

View file

@ -1,4 +1,3 @@
import Card from "@material-ui/core/Card";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody"; import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell"; import TableCell from "@material-ui/core/TableCell";
@ -65,10 +64,9 @@ const PluginList: React.FC<PluginListProps> = props => {
onPreviousPage onPreviousPage
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
return ( return (
<Card>
<ResponsiveTable> <ResponsiveTable>
<TableHead> <TableHead>
<TableCellHeader <TableCellHeader
@ -165,7 +163,6 @@ const PluginList: React.FC<PluginListProps> = props => {
)} )}
</TableBody> </TableBody>
</ResponsiveTable> </ResponsiveTable>
</Card>
); );
}; };
PluginList.displayName = "PluginList"; PluginList.displayName = "PluginList";

View file

@ -1,35 +1,85 @@
import React from "react"; import React from "react";
import Card from "@material-ui/core/Card";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { PageListProps, SortPage } from "@saleor/types"; import {
PageListProps,
SortPage,
FilterPageProps,
TabPageProps
} from "@saleor/types";
import { PluginListUrlSortField } from "@saleor/plugins/urls"; import { PluginListUrlSortField } from "@saleor/plugins/urls";
import FilterBar from "@saleor/components/FilterBar";
import { Plugins_plugins_edges_node } from "../../types/Plugins"; import { Plugins_plugins_edges_node } from "../../types/Plugins";
import PluginsList from "../PluginsList/PluginsList"; import PluginsList from "../PluginsList/PluginsList";
import {
createFilterStructure,
PluginFilterKeys,
PluginListFilterOpts
} from "./filters";
export interface PluginsListPageProps export interface PluginsListPageProps
extends PageListProps, extends PageListProps,
SortPage<PluginListUrlSortField> { FilterPageProps<PluginFilterKeys, PluginListFilterOpts>,
SortPage<PluginListUrlSortField>,
TabPageProps {
plugins: Plugins_plugins_edges_node[]; plugins: Plugins_plugins_edges_node[];
onBack: () => void; onBack: () => void;
} }
const PluginsListPage: React.FC<PluginsListPageProps> = ({ const PluginsListPage: React.FC<PluginsListPageProps> = ({
currencySymbol,
currentTab,
initialSearch,
filterOpts,
tabs,
onAdd,
onAll,
onBack, onBack,
onSearchChange,
onFilterChange,
onTabChange,
onTabDelete,
onTabSave,
...listProps ...listProps
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const filterStructure = createFilterStructure(intl, filterOpts);
return ( return (
<Container> <Container>
<AppHeader onBack={onBack}> <AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.configuration)} {intl.formatMessage(sectionNames.configuration)}
</AppHeader> </AppHeader>
<PageHeader title={intl.formatMessage(sectionNames.plugins)} /> <PageHeader title={intl.formatMessage(sectionNames.plugins)} />
<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} /> <PluginsList {...listProps} />
</Card>
</Container> </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 { default } from "./PluginsListPage";
export * from "./PluginsListPage"; export * from "./PluginsListPage";
export * from "./filters";

View file

@ -35,6 +35,7 @@ const pluginsList = gql`
$after: String $after: String
$last: Int $last: Int
$before: String $before: String
$filter: PluginFilterInput
$sort: PluginSortingInput $sort: PluginSortingInput
) { ) {
plugins( plugins(
@ -42,6 +43,7 @@ const pluginsList = gql`
after: $after after: $after
first: $first first: $first
last: $last last: $last
filter: $filter
sortBy: $sort sortBy: $sort
) { ) {
edges { edges {

View file

@ -2,7 +2,7 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // This file was automatically generated and should not be edited.
import { PluginSortingInput } from "./../../types/globalTypes"; import { PluginFilterInput, PluginSortingInput } from "./../../types/globalTypes";
// ==================================================== // ====================================================
// GraphQL query operation: Plugins // GraphQL query operation: Plugins
@ -44,5 +44,6 @@ export interface PluginsVariables {
after?: string | null; after?: string | null;
last?: number | null; last?: number | null;
before?: string | null; before?: string | null;
filter?: PluginFilterInput | null;
sort?: PluginSortingInput | null; sort?: PluginSortingInput | null;
} }

View file

@ -1,17 +1,34 @@
import { stringify as stringifyQs } from "qs"; import { stringify as stringifyQs } from "qs";
import urlJoin from "url-join"; import urlJoin from "url-join";
import { Dialog, Pagination, SingleAction, Sort } from "../types"; import {
Dialog,
Pagination,
SingleAction,
Sort,
Filters,
ActiveTab,
TabActionDialog
} from "../types";
export const pluginSection = "/plugins/"; export const pluginSection = "/plugins/";
export const pluginListPath = pluginSection; export const pluginListPath = pluginSection;
export enum PluginListUrlFiltersEnum {
active = "active",
query = "query"
}
export type PluginListUrlFilters = Filters<PluginListUrlFiltersEnum>;
export type PluginListUrlDialog = TabActionDialog;
export enum PluginListUrlSortField { export enum PluginListUrlSortField {
name = "name", name = "name",
active = "active" active = "active"
} }
export type PluginListUrlSort = Sort<PluginListUrlSortField>; export type PluginListUrlSort = Sort<PluginListUrlSortField>;
export type PluginListUrlQueryParams = Pagination & export type PluginListUrlQueryParams = ActiveTab &
Dialog<PluginListUrlDialog> &
PluginListUrlFilters &
Pagination &
PluginListUrlSort & PluginListUrlSort &
SingleAction; SingleAction;
export const pluginListUrl = (params?: PluginListUrlQueryParams) => export const pluginListUrl = (params?: PluginListUrlQueryParams) =>

View file

@ -10,10 +10,32 @@ import React from "react";
import { getSortParams } from "@saleor/utils/sort"; import { getSortParams } from "@saleor/utils/sort";
import createSortHandler from "@saleor/utils/handlers/sortHandler"; import createSortHandler from "@saleor/utils/handlers/sortHandler";
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
import useShop from "@saleor/hooks/useShop";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import SaveFilterTabDialog, {
SaveFilterTabDialogFormData
} from "@saleor/components/SaveFilterTabDialog";
import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
import PluginsListPage from "../../components/PluginsListPage/PluginsListPage"; import PluginsListPage from "../../components/PluginsListPage/PluginsListPage";
import { usePluginsListQuery } from "../../queries"; import { usePluginsListQuery } from "../../queries";
import { PluginListUrlQueryParams, pluginListUrl, pluginUrl } from "../../urls"; import {
PluginListUrlQueryParams,
pluginListUrl,
pluginUrl,
PluginListUrlDialog
} from "../../urls";
import { getSortQueryVariables } from "./sort"; import { getSortQueryVariables } from "./sort";
import {
getFilterQueryParam,
getFilterOpts,
getFilterTabs,
areFiltersApplied,
saveFilterTab,
getActiveFilters,
deleteFilterTab,
getFilterVariables
} from "./filters";
interface PluginsListProps { interface PluginsListProps {
params: PluginListUrlQueryParams; params: PluginListUrlQueryParams;
@ -22,6 +44,7 @@ interface PluginsListProps {
export const PluginsList: React.FC<PluginsListProps> = ({ params }) => { export const PluginsList: React.FC<PluginsListProps> = ({ params }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const paginate = usePaginator(); const paginate = usePaginator();
const shop = useShop();
const { updateListSettings, settings } = useListSettings( const { updateListSettings, settings } = useListSettings(
ListViews.PLUGINS_LIST ListViews.PLUGINS_LIST
); );
@ -30,6 +53,7 @@ export const PluginsList: React.FC<PluginsListProps> = ({ params }) => {
const queryVariables = React.useMemo( const queryVariables = React.useMemo(
() => ({ () => ({
...paginationState, ...paginationState,
filter: getFilterVariables(params),
sort: getSortQueryVariables(params) sort: getSortQueryVariables(params)
}), }),
[params] [params]
@ -39,6 +63,50 @@ export const PluginsList: React.FC<PluginsListProps> = ({ params }) => {
variables: queryVariables variables: queryVariables
}); });
const tabs = getFilterTabs();
const currentTab =
params.activeTab === undefined
? areFiltersApplied(params)
? tabs.length + 1
: 0
: parseInt(params.activeTab, 0);
const [
changeFilters,
resetFilters,
handleSearchChange
] = createFilterHandlers({
createUrl: pluginListUrl,
getFilterQueryParam,
navigate,
params
});
const [openModal, closeModal] = createDialogActionHandlers<
PluginListUrlDialog,
PluginListUrlQueryParams
>(navigate, pluginListUrl, params);
const handleTabChange = (tab: number) => {
navigate(
pluginListUrl({
activeTab: tab.toString(),
...getFilterTabs()[tab - 1].data
})
);
};
const handleFilterTabDelete = () => {
deleteFilterTab(currentTab);
navigate(pluginListUrl());
};
const handleFilterTabSave = (data: SaveFilterTabDialogFormData) => {
saveFilterTab(data.name, getActiveFilters(params));
handleTabChange(tabs.length + 1);
};
const { loadNextPage, loadPreviousPage, pageInfo } = paginate( const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
maybe(() => data.plugins.pageInfo), maybe(() => data.plugins.pageInfo),
paginationState, paginationState,
@ -46,23 +114,48 @@ export const PluginsList: React.FC<PluginsListProps> = ({ params }) => {
); );
const handleSort = createSortHandler(navigate, pluginListUrl, params); const handleSort = createSortHandler(navigate, pluginListUrl, params);
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
return ( return (
<> <>
<PluginsListPage <PluginsListPage
currencySymbol={currencySymbol}
currentTab={currentTab}
disabled={loading} disabled={loading}
filterOpts={getFilterOpts(params)}
initialSearch={params.query || ""}
settings={settings} settings={settings}
plugins={maybe(() => data.plugins.edges.map(edge => edge.node))} plugins={maybe(() => data.plugins.edges.map(edge => edge.node))}
pageInfo={pageInfo} pageInfo={pageInfo}
sort={getSortParams(params)} sort={getSortParams(params)}
tabs={getFilterTabs().map(tab => tab.name)}
onAdd={() => navigate(configurationMenuUrl)} onAdd={() => navigate(configurationMenuUrl)}
onAll={resetFilters}
onBack={() => navigate(configurationMenuUrl)} onBack={() => navigate(configurationMenuUrl)}
onFilterChange={changeFilters}
onSearchChange={handleSearchChange}
onNextPage={loadNextPage} onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage} onPreviousPage={loadPreviousPage}
onSort={handleSort} onSort={handleSort}
onTabSave={() => openModal("save-search")}
onTabDelete={() => openModal("delete-search")}
onTabChange={handleTabChange}
onUpdateListSettings={updateListSettings} onUpdateListSettings={updateListSettings}
onRowClick={id => () => navigate(pluginUrl(id))} onRowClick={id => () => navigate(pluginUrl(id))}
/> />
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleFilterTabSave}
/>
<DeleteFilterTabDialog
open={params.action === "delete-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleFilterTabDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
/>
</> </>
); );
}; };

View file

@ -0,0 +1,71 @@
import { PluginFilterInput } from "@saleor/types/globalTypes";
import {
PluginListFilterOpts,
PluginFilterKeys
} from "@saleor/plugins/components/PluginsListPage";
import { maybe, parseBoolean } from "@saleor/misc";
import { IFilterElement } from "@saleor/components/Filter";
import {
PluginListUrlFilters,
PluginListUrlFiltersEnum,
PluginListUrlQueryParams
} from "../../urls";
import {
createFilterTabUtils,
createFilterUtils
} from "../../../utils/filters";
export const PLUGIN_FILTERS_KEY = "pluginFilters";
export function getFilterOpts(
params: PluginListUrlFilters
): PluginListFilterOpts {
return {
isActive: {
active: maybe(() => params.active !== undefined, false),
value:
params.active !== undefined ? parseBoolean(params.active, true) : true
}
};
}
export function getFilterVariables(
params: PluginListUrlFilters
): PluginFilterInput {
return {
active:
params.active !== undefined
? parseBoolean(params.active, true)
: undefined,
search: params.query
};
}
export function getFilterQueryParam(
filter: IFilterElement<PluginFilterKeys>
): PluginListUrlFilters {
const { active, name, value } = filter;
switch (name) {
case PluginFilterKeys.active:
if (!active) {
return {
active: undefined
};
}
return {
active: value[0]
};
}
}
export const {
deleteFilterTab,
getFilterTabs,
saveFilterTab
} = createFilterTabUtils<PluginListUrlFilters>(PLUGIN_FILTERS_KEY);
export const { areFiltersApplied, getActiveFilters } = createFilterUtils<
PluginListUrlQueryParams,
PluginListUrlFilters
>(PluginListUrlFiltersEnum);

View file

@ -986,6 +986,11 @@ export interface PageTranslationInput {
contentJson?: any | null; contentJson?: any | null;
} }
export interface PluginFilterInput {
active?: boolean | null;
search?: string | null;
}
export interface PluginSortingInput { export interface PluginSortingInput {
direction: OrderDirection; direction: OrderDirection;
field: PluginSortField; field: PluginSortField;