Content list datagrid (#4024)

* Build types

* (wip) Page list datagrid implementation

* Bulk actions

* Add changeset
This commit is contained in:
Michał Droń 2023-08-01 10:30:05 +02:00 committed by GitHub
parent 63f2ef0a18
commit 0995b02dfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 638 additions and 499 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": minor
---
Introduce read-only datagrid on content view

View file

@ -388,6 +388,10 @@
"context": "menu item name", "context": "menu item name",
"string": "Name" "string": "Name"
}, },
"0XHXTZ": {
"context": "column header",
"string": "Title"
},
"0a0fLZ": { "0a0fLZ": {
"context": "countries list menu label when no countries are assigned", "context": "countries list menu label when no countries are assigned",
"string": "There are no countries assigned" "string": "There are no countries assigned"
@ -1028,10 +1032,6 @@
"5BajZK": { "5BajZK": {
"string": "View and update your site settings" "string": "View and update your site settings"
}, },
"5GSYCR": {
"context": "page status",
"string": "Visibility"
},
"5HwLx9": { "5HwLx9": {
"context": "alert", "context": "alert",
"string": "Warehouse limit reached" "string": "Warehouse limit reached"
@ -2343,10 +2343,6 @@
"F7DxHw": { "F7DxHw": {
"string": "Subcategories" "string": "Subcategories"
}, },
"F8gsds": {
"context": "unpublish page, button",
"string": "Unpublish"
},
"FA+MRz": { "FA+MRz": {
"string": "Set plugin as active" "string": "Set plugin as active"
}, },
@ -2804,10 +2800,6 @@
"context": "order refund amount", "context": "order refund amount",
"string": "Max Refund" "string": "Max Refund"
}, },
"I8dAAe": {
"context": "page internal name",
"string": "Slug"
},
"I8mqqj": { "I8mqqj": {
"context": "PageTypeDeleteWarningDialog single assigned items button label", "context": "PageTypeDeleteWarningDialog single assigned items button label",
"string": "View pages" "string": "View pages"
@ -4505,10 +4497,6 @@
"UN+yTt": { "UN+yTt": {
"string": "Staff Settings" "string": "Staff Settings"
}, },
"UN3qWD": {
"context": "page status",
"string": "Not Published"
},
"UNwG+4": { "UNwG+4": {
"context": "dialog content", "context": "dialog content",
"string": "{counter,plural,one{Are you sure you want to delete this page?} other{Are you sure you want to delete {displayQuantity} pages?}}" "string": "{counter,plural,one{Are you sure you want to delete this page?} other{Are you sure you want to delete {displayQuantity} pages?}}"
@ -4635,10 +4623,6 @@
"context": "button", "context": "button",
"string": "Create Permission Group" "string": "Create Permission Group"
}, },
"V2+HTM": {
"context": "dialog header",
"string": "Title"
},
"V2BBQu": { "V2BBQu": {
"string": "Currency in both channels must be the same" "string": "Currency in both channels must be the same"
}, },
@ -4890,6 +4874,10 @@
"context": "error message", "context": "error message",
"string": "Login went wrong. Please try again." "string": "Login went wrong. Please try again."
}, },
"WvMDHa": {
"context": "page status",
"string": "Not published"
},
"Ww69SE": { "Ww69SE": {
"context": "search input placeholder", "context": "search input placeholder",
"string": "Search tax classes" "string": "Search tax classes"
@ -6119,6 +6107,10 @@
"context": "button, form submit, grant refund create", "context": "button, form submit, grant refund create",
"string": "Grant refund" "string": "Grant refund"
}, },
"ftOPoy": {
"context": "bulk actions button label",
"string": "Unpublish"
},
"ftcHpD": { "ftcHpD": {
"string": "Customer created" "string": "Customer created"
}, },
@ -6295,6 +6287,10 @@
"hJZwTS": { "hJZwTS": {
"string": "Email address" "string": "Email address"
}, },
"hJrzlT": {
"context": "tab name",
"string": "All content"
},
"hLOEeb": { "hLOEeb": {
"context": "button", "context": "button",
"string": "Set as default billing address" "string": "Set as default billing address"
@ -6513,6 +6509,10 @@
"context": "label", "context": "label",
"string": "Search pages" "string": "Search pages"
}, },
"j/Oo0B": {
"context": "bulk actions button label",
"string": "Publish"
},
"j/vV0n": { "j/vV0n": {
"context": "channel name", "context": "channel name",
"string": "Channel Name" "string": "Channel Name"
@ -6697,6 +6697,10 @@
"kFsTMN": { "kFsTMN": {
"string": "Delete customers" "string": "Delete customers"
}, },
"kIcyUo": {
"context": "column header",
"string": "Slug"
},
"kIvvax": { "kIvvax": {
"string": "Search Products..." "string": "Search Products..."
}, },
@ -8367,6 +8371,10 @@
"wL7VAE": { "wL7VAE": {
"string": "Actions" "string": "Actions"
}, },
"wLB8B3": {
"context": "column header",
"string": "Visibility"
},
"wNQzS/": { "wNQzS/": {
"string": "The primary address of this customer." "string": "The primary address of this customer."
}, },
@ -8573,10 +8581,6 @@
"context": "dialog content", "context": "dialog content",
"string": "{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}" "string": "{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}"
}, },
"yEmwxD": {
"context": "publish page, button",
"string": "Publish"
},
"yH56V+": { "yH56V+": {
"string": "Ooops!..." "string": "Ooops!..."
}, },
@ -8767,6 +8771,10 @@
"context": "sum of captured amount of all transactions", "context": "sum of captured amount of all transactions",
"string": "Total captured" "string": "Total captured"
}, },
"zfBsje": {
"context": "bulk actions button label",
"string": "Delete content"
},
"zfjAc7": { "zfjAc7": {
"context": "grant refund, card header", "context": "grant refund, card header",
"string": "Unfulfilled products" "string": "Unfulfilled products"

View file

@ -96,6 +96,7 @@ export const defaultListSettings: AppListViewSettings = {
}, },
[ListViews.PAGES_LIST]: { [ListViews.PAGES_LIST]: {
rowNumber: PAGINATE_BY, rowNumber: PAGINATE_BY,
columns: ["title", "slug", "visible"],
}, },
[ListViews.PLUGINS_LIST]: { [ListViews.PLUGINS_LIST]: {
rowNumber: PAGINATE_BY, rowNumber: PAGINATE_BY,

View file

@ -6,7 +6,10 @@ import {
useBulkRemoveCustomersMutation, useBulkRemoveCustomersMutation,
useListCustomersQuery, useListCustomersQuery,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import {
getPresetNameToDelete,
useFilterPresets,
} from "@dashboard/hooks/useFilterPresets";
import useListSettings from "@dashboard/hooks/useListSettings"; import useListSettings from "@dashboard/hooks/useListSettings";
import useNavigator from "@dashboard/hooks/useNavigator"; import useNavigator from "@dashboard/hooks/useNavigator";
import useNotifier from "@dashboard/hooks/useNotifier"; import useNotifier from "@dashboard/hooks/useNotifier";
@ -38,7 +41,6 @@ import {
getFilterOpts, getFilterOpts,
getFilterQueryParam, getFilterQueryParam,
getFilterVariables, getFilterVariables,
getPresetNameToDelete,
storageUtils, storageUtils,
} from "./filters"; } from "./filters";
import { getSortQueryVariables } from "./sort"; import { getSortQueryVariables } from "./sort";

View file

@ -8,7 +8,6 @@ import { CustomerFilterInput } from "@dashboard/graphql";
import { import {
createFilterTabUtils, createFilterTabUtils,
createFilterUtils, createFilterUtils,
GetFilterTabsOutput,
getGteLteVariables, getGteLteVariables,
getMinMaxQueryParam, getMinMaxQueryParam,
} from "../../../utils/filters"; } from "../../../utils/filters";
@ -95,14 +94,3 @@ export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
createFilterUtils<CustomerListUrlQueryParams, CustomerListUrlFilters>( createFilterUtils<CustomerListUrlQueryParams, CustomerListUrlFilters>(
CustomerListUrlFiltersEnum, CustomerListUrlFiltersEnum,
); );
export const getPresetNameToDelete = (
presets: GetFilterTabsOutput<string>,
presetIdToDelete: number | null,
): string => {
const presetIndex = presetIdToDelete ? presetIdToDelete - 1 : 0;
const preset = presets?.[presetIndex];
const tabName = preset?.name ?? "...";
return tabName;
};

View file

@ -134,3 +134,14 @@ export const useFilterPresets = <
hasPresetsChanged, hasPresetsChanged,
}; };
}; };
export const getPresetNameToDelete = (
presets: GetFilterTabsOutput<string>,
presetIdToDelete: number | null,
): string => {
const presetIndex = presetIdToDelete ? presetIdToDelete - 1 : 0;
const preset = presets?.[presetIndex];
const tabName = preset?.name ?? "...";
return tabName;
};

View file

@ -1,211 +0,0 @@
// @ts-strict-ignore
import Checkbox from "@dashboard/components/Checkbox";
import { Pill } from "@dashboard/components/Pill";
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton";
import TableCellHeader from "@dashboard/components/TableCellHeader";
import TableHead from "@dashboard/components/TableHead";
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
import TableRowLink from "@dashboard/components/TableRowLink";
import { PageFragment } from "@dashboard/graphql";
import { maybe, renderCollection } from "@dashboard/misc";
import { PageListUrlSortField, pageUrl } from "@dashboard/pages/urls";
import { ListActions, ListProps, SortPage } from "@dashboard/types";
import { getArrowDirection } from "@dashboard/utils/sort";
import { Card, TableBody, TableCell, TableFooter } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
export interface PageListProps
extends ListProps,
ListActions,
SortPage<PageListUrlSortField> {
pages: PageFragment[];
}
const useStyles = makeStyles(
theme => ({
[theme.breakpoints.up("lg")]: {
colSlug: {
width: 250,
},
colTitle: {},
colVisibility: {
width: 200,
},
},
colSlug: {},
colTitle: {
paddingLeft: 0,
},
colVisibility: {},
link: {
cursor: "pointer",
},
}),
{ name: "PageList" },
);
const numberOfColumns = 4;
const PageList: React.FC<PageListProps> = props => {
const {
settings,
pages,
disabled,
onSort,
onUpdateListSettings,
isChecked,
selected,
sort,
toggle,
toggleAll,
toolbar,
} = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<Card>
<ResponsiveTable>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={pages}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCellHeader
direction={
sort.sort === PageListUrlSortField.title
? getArrowDirection(sort.asc)
: undefined
}
arrowPosition="right"
onClick={() => onSort(PageListUrlSortField.title)}
className={classes.colTitle}
>
<FormattedMessage
id="V2+HTM"
defaultMessage="Title"
description="dialog header"
/>
</TableCellHeader>
<TableCellHeader
direction={
sort.sort === PageListUrlSortField.slug
? getArrowDirection(sort.asc)
: undefined
}
arrowPosition="right"
onClick={() => onSort(PageListUrlSortField.slug)}
className={classes.colSlug}
>
<FormattedMessage
id="I8dAAe"
defaultMessage="Slug"
description="page internal name"
/>
</TableCellHeader>
<TableCellHeader
direction={
sort.sort === PageListUrlSortField.visible
? getArrowDirection(sort.asc)
: undefined
}
arrowPosition="right"
onClick={() => onSort(PageListUrlSortField.visible)}
className={classes.colVisibility}
>
<FormattedMessage
id="5GSYCR"
defaultMessage="Visibility"
description="page status"
/>
</TableCellHeader>
</TableHead>
<TableFooter>
<TableRowLink>
<TablePaginationWithContext
colSpan={numberOfColumns}
settings={settings}
disabled={disabled}
onUpdateListSettings={onUpdateListSettings}
/>
</TableRowLink>
</TableFooter>
<TableBody>
{renderCollection(
pages,
page => {
const isSelected = page ? isChecked(page.id) : false;
return (
<TableRowLink
hover={!!page}
className={!!page ? classes.link : undefined}
href={page && pageUrl(page.id)}
key={page ? page.id : "skeleton"}
selected={isSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(page.id)}
/>
</TableCell>
<TableCell className={classes.colTitle}>
{maybe<React.ReactNode>(() => page.title, <Skeleton />)}
</TableCell>
<TableCell className={classes.colSlug}>
{maybe<React.ReactNode>(() => page.slug, <Skeleton />)}
</TableCell>
<TableCell className={classes.colVisibility}>
{maybe<React.ReactNode>(
() => (
<Pill
label={
page.isPublished
? intl.formatMessage({
id: "G1KzEx",
defaultMessage: "Published",
description: "page status",
})
: intl.formatMessage({
id: "UN3qWD",
defaultMessage: "Not Published",
description: "page status",
})
}
color={page.isPublished ? "success" : "error"}
/>
),
<Skeleton />,
)}
</TableCell>
</TableRowLink>
);
},
() => (
<TableRowLink>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage
id="iMJka8"
defaultMessage="No pages found"
/>
</TableCell>
</TableRowLink>
),
)}
</TableBody>
</ResponsiveTable>
</Card>
);
};
PageList.displayName = "PageList";
export default PageList;

View file

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

View file

@ -0,0 +1,156 @@
import { ColumnPicker } from "@dashboard/components/Datagrid/ColumnPicker/ColumnPicker";
import { useColumns } from "@dashboard/components/Datagrid/ColumnPicker/useColumns";
import Datagrid from "@dashboard/components/Datagrid/Datagrid";
import {
DatagridChangeStateContext,
useDatagridChangeState,
} from "@dashboard/components/Datagrid/hooks/useDatagridChange";
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
import { Page, Pages } from "@dashboard/pages/types";
import { PageListUrlSortField } from "@dashboard/pages/urls";
import { ListProps, SortPage } from "@dashboard/types";
import { Item } from "@glideapps/glide-data-grid";
import { Box, useTheme } from "@saleor/macaw-ui/next";
import React, { useCallback, useMemo } from "react";
import { useIntl } from "react-intl";
import { createGetCellContent, pageListStaticColumnsAdapter } from "./datagrid";
import { messages } from "./messages";
interface PageListDatagridProps
extends ListProps,
SortPage<PageListUrlSortField> {
pages: Pages | undefined;
loading: boolean;
hasRowHover?: boolean;
onSelectPageIds: (rowsIndex: number[], clearSelection: () => void) => void;
onRowClick: (id: string) => void;
rowAnchor?: (id: string) => string;
}
export const PageListDatagrid = ({
pages,
sort,
loading,
settings,
onUpdateListSettings,
hasRowHover,
onRowClick,
rowAnchor,
onSelectPageIds,
onSort,
}: PageListDatagridProps) => {
const intl = useIntl();
const datagrid = useDatagridChangeState();
const pageListStaticColumns = useMemo(
() => pageListStaticColumnsAdapter(intl, sort),
[intl, sort],
);
const onColumnChange = useCallback(
(picked: string[]) => {
if (onUpdateListSettings) {
onUpdateListSettings("columns", picked.filter(Boolean));
}
},
[onUpdateListSettings],
);
const {
handlers,
visibleColumns,
staticColumns,
selectedColumns,
recentlyAddedColumn,
} = useColumns({
staticColumns: pageListStaticColumns,
selectedColumns: settings?.columns ?? [],
onSave: onColumnChange,
});
const { themeValues } = useTheme();
const getCellContent = useCallback(
createGetCellContent({
pages,
columns: visibleColumns,
intl,
themeValues,
}),
[pages, visibleColumns],
);
const handleRowClick = useCallback(
([_, row]: Item) => {
if (!onRowClick || !pages) {
return;
}
const rowData: Page = pages[row];
onRowClick(rowData.id);
},
[onRowClick, pages],
);
const handleRowAnchor = useCallback(
([, row]: Item) => {
if (!rowAnchor || !pages) {
return "";
}
const rowData: Page = pages[row];
return rowAnchor(rowData.id);
},
[rowAnchor, pages],
);
const handleHeaderClick = useCallback(
(col: number) => {
const columnName = visibleColumns[col].id as PageListUrlSortField;
onSort(columnName);
},
[visibleColumns, onSort],
);
return (
<DatagridChangeStateContext.Provider value={datagrid}>
<Datagrid
readonly
loading={loading}
rowMarkers="checkbox"
columnSelect="single"
hasRowHover={hasRowHover}
onColumnMoved={handlers.onMove}
onColumnResize={handlers.onResize}
verticalBorder={col => col > 0}
rows={pages?.length ?? 0}
availableColumns={visibleColumns}
emptyText={intl.formatMessage(messages.empty)}
onRowSelectionChange={onSelectPageIds}
getCellContent={getCellContent}
getCellError={() => false}
selectionActions={() => null}
menuItems={() => []}
onRowClick={handleRowClick}
onHeaderClicked={handleHeaderClick}
rowAnchor={handleRowAnchor}
recentlyAddedColumn={recentlyAddedColumn}
renderColumnPicker={() => (
<ColumnPicker
staticColumns={staticColumns}
selectedColumns={selectedColumns}
onToggle={handlers.onToggle}
/>
)}
/>
<Box paddingX={6}>
<TablePaginationWithContext
component="div"
settings={settings}
disabled={loading}
onUpdateListSettings={onUpdateListSettings}
/>
</Box>
</DatagridChangeStateContext.Provider>
);
};

View file

@ -0,0 +1,88 @@
import {
readonlyTextCell,
tagsCell,
} from "@dashboard/components/Datagrid/customCells/cells";
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
import { getStatusColor } from "@dashboard/misc";
import { Pages } from "@dashboard/pages/types";
import { PageListUrlSortField } from "@dashboard/pages/urls";
import { Sort } from "@dashboard/types";
import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon";
import { GridCell, Item } from "@glideapps/glide-data-grid";
import { ThemeTokensValues } from "@saleor/macaw-ui/next";
import { IntlShape } from "react-intl";
import { columnsMessages, messages } from "./messages";
export const pageListStaticColumnsAdapter = (
intl: IntlShape,
sort: Sort<PageListUrlSortField>,
): AvailableColumn[] =>
[
{
id: "title",
title: intl.formatMessage(columnsMessages.title),
width: 450,
},
{
id: "slug",
title: intl.formatMessage(columnsMessages.slug),
width: 300,
},
{
id: "visible",
title: intl.formatMessage(columnsMessages.visible),
width: 300,
},
].map(column => ({
...column,
icon: getColumnSortDirectionIcon(sort, column.id),
}));
export const createGetCellContent =
({
pages,
columns,
intl,
themeValues,
}: {
pages: Pages | undefined;
columns: AvailableColumn[];
intl: IntlShape;
themeValues: ThemeTokensValues;
}) =>
([column, row]: Item): GridCell => {
const rowData = pages?.[row];
const columnId = columns[column]?.id;
if (!columnId || !rowData) {
return readonlyTextCell("");
}
switch (columnId) {
case "title":
return readonlyTextCell(rowData?.title ?? "");
case "slug":
return readonlyTextCell(rowData?.slug ?? "");
case "visible":
const tag = rowData?.isPublished
? intl.formatMessage(messages.published)
: intl.formatMessage(messages.notPublished);
return tagsCell(
[
{
tag,
color:
themeValues.colors.background[
getStatusColor(
rowData?.isPublished ? "success" : "error",
) as keyof ThemeTokensValues["colors"]["background"]
],
},
],
[tag],
);
default:
return readonlyTextCell("");
}
};

View file

@ -0,0 +1,36 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
empty: {
id: "iMJka8",
defaultMessage: "No pages found",
},
published: {
id: "G1KzEx",
defaultMessage: "Published",
description: "page status",
},
notPublished: {
id: "WvMDHa",
defaultMessage: "Not published",
description: "page status",
},
});
export const columnsMessages = defineMessages({
title: {
id: "0XHXTZ",
defaultMessage: "Title",
description: "column header",
},
slug: {
id: "kIcyUo",
defaultMessage: "Slug",
description: "column header",
},
visible: {
id: "wLB8B3",
defaultMessage: "Visibility",
description: "column header",
},
});

View file

@ -1,7 +1,10 @@
// @ts-strict-ignore // @ts-strict-ignore
import { import {
filterPageProps,
filterPresetsProps,
listActionsProps, listActionsProps,
pageListProps, pageListProps,
searchPageProps,
sortPageProps, sortPageProps,
} from "@dashboard/fixtures"; } from "@dashboard/fixtures";
import { pageList } from "@dashboard/pages/fixtures"; import { pageList } from "@dashboard/pages/fixtures";
@ -12,21 +15,37 @@ import { PaginatorContextDecorator } from "../../../../.storybook/decorators";
import PageListPage, { PageListPageProps } from "./PageListPage"; import PageListPage, { PageListPageProps } from "./PageListPage";
const props: PageListPageProps = { const props: PageListPageProps = {
...filterPageProps,
...listActionsProps, ...listActionsProps,
...pageListProps.default, ...pageListProps.default,
...searchPageProps,
...sortPageProps, ...sortPageProps,
...filterPresetsProps,
settings: {
...pageListProps.default.settings,
columns: ["title", "slug", "visible"],
},
pages: pageList, pages: pageList,
sort: { sort: {
...sortPageProps.sort, ...sortPageProps.sort,
sort: PageListUrlSortField.title, sort: PageListUrlSortField.title,
}, },
actionDialogOpts: { filterOpts: {
open: () => undefined, pageType: {
close: () => undefined, active: false,
}, value: [],
params: { choices: [],
ids: [], displayValues: [],
},
}, },
selectedPageIds: [],
loading: false,
hasPresetsChanged: () => false,
onSelectPageIds: () => undefined,
onPagesDelete: () => undefined,
onPagesPublish: () => undefined,
onPagesUnpublish: () => undefined,
onPageCreate: () => undefined,
}; };
const meta: Meta<typeof PageListPage> = { const meta: Meta<typeof PageListPage> = {

View file

@ -1,19 +1,34 @@
import { ListFilters } from "@dashboard/components/AppLayout/ListFilters";
import { TopNav } from "@dashboard/components/AppLayout/TopNav"; import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { Button } from "@dashboard/components/Button"; import { BulkDeleteButton } from "@dashboard/components/BulkDeleteButton";
import { PageFragment } from "@dashboard/graphql"; import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
import { ListPageLayout } from "@dashboard/components/Layouts";
import useNavigator from "@dashboard/hooks/useNavigator";
import { sectionNames } from "@dashboard/intl"; import { sectionNames } from "@dashboard/intl";
import { Pages } from "@dashboard/pages/types";
import { import {
PageListUrlDialog, PageListUrlDialog,
PageListUrlQueryParams, PageListUrlQueryParams,
PageListUrlSortField, PageListUrlSortField,
pageUrl,
} from "@dashboard/pages/urls"; } from "@dashboard/pages/urls";
import { ListActions, PageListProps, SortPage } from "@dashboard/types"; import {
FilterPagePropsWithPresets,
PageListProps,
SortPage,
} from "@dashboard/types";
import { Card } from "@material-ui/core"; import { Card } from "@material-ui/core";
import { Box, Button, ChevronRightIcon } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import PageList from "../PageList"; import {
import PageListSearchAndFilters from "./PageListSearchAndFilters"; createFilterStructure,
PageListFilterKeys,
PageListFilterOpts,
} from "../../views/PageList/filters";
import { PageListDatagrid } from "../PageListDatagrid/PageListDatagrid";
import { pagesListSearchAndFiltersMessages as messages } from "./messages";
export interface PageListActionDialogOpts { export interface PageListActionDialogOpts {
open: (action: PageListUrlDialog, newParams?: PageListUrlQueryParams) => void; open: (action: PageListUrlDialog, newParams?: PageListUrlQueryParams) => void;
@ -21,41 +36,125 @@ export interface PageListActionDialogOpts {
} }
export interface PageListPageProps export interface PageListPageProps
extends PageListProps, extends PageListProps,
ListActions, FilterPagePropsWithPresets<PageListFilterKeys, PageListFilterOpts>,
SortPage<PageListUrlSortField> { SortPage<PageListUrlSortField> {
pages: PageFragment[]; pages: Pages | undefined;
params: PageListUrlQueryParams; selectedPageIds: string[];
actionDialogOpts: PageListActionDialogOpts; loading: boolean;
onAdd: () => void; onSelectPageIds: (rows: number[], clearSelection: () => void) => void;
onPagesDelete: () => void;
onPagesPublish: () => void;
onPagesUnpublish: () => void;
onPageCreate: () => void;
} }
const PageListPage: React.FC<PageListPageProps> = ({ const PageListPage: React.FC<PageListPageProps> = ({
params, selectedFilterPreset,
actionDialogOpts, filterOpts,
onAdd, initialSearch,
onFilterPresetsAll,
onFilterChange,
onFilterPresetDelete,
onFilterPresetUpdate,
onSearchChange,
onFilterPresetChange,
onFilterPresetPresetSave,
filterPresets,
selectedPageIds,
hasPresetsChanged,
onPagesDelete,
onPagesPublish,
onPagesUnpublish,
onPageCreate,
...listProps ...listProps
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const navigate = useNavigator();
const structure = createFilterStructure(intl, filterOpts);
const [isFilterPresetOpen, setFilterPresetOpen] = React.useState(false);
return ( return (
<> <ListPageLayout>
<TopNav title={intl.formatMessage(sectionNames.content)}> <TopNav
<Button onClick={onAdd} variant="primary" data-test-id="create-page"> title={intl.formatMessage(sectionNames.content)}
<FormattedMessage isAlignToRight={false}
id="DOVEZF" withoutBorder
defaultMessage="Create content" >
description="button" <Box
/> __flex={1}
</Button> display="flex"
justifyContent="space-between"
alignItems="center"
>
<Box display="flex">
<Box marginX={5} display="flex" alignItems="center">
<ChevronRightIcon />
</Box>
<FilterPresetsSelect
presetsChanged={hasPresetsChanged()}
onSelect={onFilterPresetChange}
onRemove={onFilterPresetDelete}
onUpdate={onFilterPresetUpdate}
savedPresets={filterPresets}
activePreset={selectedFilterPreset}
onSelectAll={onFilterPresetsAll}
onSave={onFilterPresetPresetSave}
isOpen={isFilterPresetOpen}
onOpenChange={setFilterPresetOpen}
selectAllLabel={intl.formatMessage({
id: "hJrzlT",
defaultMessage: "All content",
description: "tab name",
})}
/>
</Box>
<Box display="flex" alignItems="center" gap={2}>
<Button
onClick={onPageCreate}
variant="primary"
data-test-id="create-page"
>
<FormattedMessage
id="DOVEZF"
defaultMessage="Create content"
description="button"
/>
</Button>
</Box>
</Box>
</TopNav> </TopNav>
<Card> <Card>
<PageListSearchAndFilters <ListFilters
params={params} filterStructure={structure}
actionDialogOpts={actionDialogOpts} initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage(messages.searchPlaceholder)}
onFilterChange={onFilterChange}
onSearchChange={onSearchChange}
actions={
selectedPageIds.length > 0 && (
<Box display="flex" gap={4}>
<Button variant="secondary" onClick={onPagesUnpublish}>
<FormattedMessage {...messages.unpublish} />
</Button>
<Button variant="secondary" onClick={onPagesPublish}>
<FormattedMessage {...messages.publish} />
</Button>
<BulkDeleteButton onClick={onPagesDelete}>
<FormattedMessage {...messages.delete} />
</BulkDeleteButton>
</Box>
)
}
/>
<PageListDatagrid
{...listProps}
hasRowHover={!isFilterPresetOpen}
rowAnchor={pageUrl}
onRowClick={id => navigate(pageUrl(id))}
/> />
<PageList {...listProps} />
</Card> </Card>
</> </ListPageLayout>
); );
}; };
PageListPage.displayName = "PageListPage"; PageListPage.displayName = "PageListPage";

View file

@ -1,136 +0,0 @@
// @ts-strict-ignore
import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog";
import FilterBar from "@dashboard/components/FilterBar";
import SaveFilterTabDialog, {
SaveFilterTabDialogFormData,
} from "@dashboard/components/SaveFilterTabDialog";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@dashboard/config";
import { getSearchFetchMoreProps } from "@dashboard/hooks/makeTopLevelSearch/utils";
import useBulkActions from "@dashboard/hooks/useBulkActions";
import useNavigator from "@dashboard/hooks/useNavigator";
import { pageListUrl, PageListUrlQueryParams } from "@dashboard/pages/urls";
import usePageTypeSearch from "@dashboard/searches/usePageTypeSearch";
import createFilterHandlers from "@dashboard/utils/handlers/filterHandlers";
import { mapEdgesToItems } from "@dashboard/utils/maps";
import React from "react";
import { useIntl } from "react-intl";
import {
createFilterStructure,
deleteFilterTab,
getActiveFilters,
getFilterOpts,
getFilterQueryParam,
getFiltersCurrentTab,
getFilterTabs,
saveFilterTab,
} from "./filters";
import { pagesListSearchAndFiltersMessages as messages } from "./messages";
import { PageListActionDialogOpts } from "./PageListPage";
interface PageListSearchAndFiltersProps {
params: PageListUrlQueryParams;
actionDialogOpts: PageListActionDialogOpts;
}
const PageListSearchAndFilters: React.FC<PageListSearchAndFiltersProps> = ({
params,
actionDialogOpts,
}) => {
const navigate = useNavigator();
const intl = useIntl();
const defaultSearchVariables = {
variables: {
...DEFAULT_INITIAL_SEARCH_DATA,
first: 5,
},
};
const { reset } = useBulkActions(params.ids);
const {
loadMore: fetchMorePageTypes,
search: searchPageTypes,
result: searchPageTypesResult,
} = usePageTypeSearch(defaultSearchVariables);
const filterOpts = getFilterOpts({
params,
pageTypes: mapEdgesToItems(searchPageTypesResult?.data?.search),
pageTypesProps: {
...getSearchFetchMoreProps(searchPageTypesResult, fetchMorePageTypes),
onSearchChange: searchPageTypes,
},
});
const [changeFilters, resetFilters, handleSearchChange] =
createFilterHandlers({
createUrl: pageListUrl,
getFilterQueryParam,
navigate,
params,
cleanupFn: reset,
});
const filterStructure = createFilterStructure(intl, filterOpts);
const { open: openModal, close: closeModal } = actionDialogOpts;
const handleTabChange = (tab: number) => {
navigate(
pageListUrl({
activeTab: tab.toString(),
...getFilterTabs()[tab - 1].data,
}),
);
};
const tabs = getFilterTabs();
const currentTab = getFiltersCurrentTab(params, tabs);
const handleTabSave = (data: SaveFilterTabDialogFormData) => {
saveFilterTab(data.name, getActiveFilters(params));
handleTabChange(tabs.length + 1);
};
const handleTabDelete = () => {
deleteFilterTab(currentTab);
reset();
navigate(pageListUrl());
};
return (
<>
<FilterBar
filterStructure={filterStructure}
initialSearch={""}
onAll={resetFilters}
onFilterChange={changeFilters}
onSearchChange={handleSearchChange}
searchPlaceholder={intl.formatMessage(messages.searchPlaceholder)}
allTabLabel={"All Content"}
tabs={tabs.map(({ name }) => name)}
currentTab={currentTab}
onTabDelete={handleTabDelete}
onTabChange={handleTabChange}
onTabSave={() => openModal("save-search")}
/>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabSave}
/>
<DeleteFilterTabDialog
open={params.action === "delete-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabDelete}
tabName={tabs[currentTab - 1]?.name ?? "..."}
/>
</>
);
};
export default PageListSearchAndFilters;

View file

@ -6,4 +6,19 @@ export const pagesListSearchAndFiltersMessages = defineMessages({
defaultMessage: "Search Content", defaultMessage: "Search Content",
description: "search content placeholder", description: "search content placeholder",
}, },
publish: {
id: "j/Oo0B",
defaultMessage: "Publish",
description: "bulk actions button label",
},
unpublish: {
id: "ftOPoy",
defaultMessage: "Unpublish",
description: "bulk actions button label",
},
delete: {
id: "zfBsje",
defaultMessage: "Delete content",
description: "bulk actions button label",
},
}); });

5
src/pages/types.ts Normal file
View file

@ -0,0 +1,5 @@
import { PageListQuery } from "@dashboard/graphql";
import { RelayToFlat } from "@dashboard/types";
export type Pages = RelayToFlat<NonNullable<PageListQuery["pages"]>>;
export type Page = Pages[number];

View file

@ -1,13 +1,18 @@
// @ts-strict-ignore // @ts-strict-ignore
import ActionDialog from "@dashboard/components/ActionDialog"; import ActionDialog from "@dashboard/components/ActionDialog";
import { Button } from "@dashboard/components/Button"; import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog";
import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@dashboard/config"; import { DEFAULT_INITIAL_SEARCH_DATA } from "@dashboard/config";
import { import {
usePageBulkPublishMutation, usePageBulkPublishMutation,
usePageBulkRemoveMutation, usePageBulkRemoveMutation,
usePageListQuery, usePageListQuery,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import useBulkActions from "@dashboard/hooks/useBulkActions"; import { getSearchFetchMoreProps } from "@dashboard/hooks/makeTopLevelSearch/utils";
import {
getPresetNameToDelete,
useFilterPresets,
} from "@dashboard/hooks/useFilterPresets";
import useListSettings from "@dashboard/hooks/useListSettings"; import useListSettings from "@dashboard/hooks/useListSettings";
import useNavigator from "@dashboard/hooks/useNavigator"; import useNavigator from "@dashboard/hooks/useNavigator";
import useNotifier from "@dashboard/hooks/useNotifier"; import useNotifier from "@dashboard/hooks/useNotifier";
@ -16,17 +21,18 @@ import usePaginator, {
createPaginationState, createPaginationState,
PaginatorContext, PaginatorContext,
} from "@dashboard/hooks/usePaginator"; } from "@dashboard/hooks/usePaginator";
import { maybe } from "@dashboard/misc"; import { useRowSelection } from "@dashboard/hooks/useRowSelection";
import PageTypePickerDialog from "@dashboard/pages/components/PageTypePickerDialog"; import PageTypePickerDialog from "@dashboard/pages/components/PageTypePickerDialog";
import usePageTypeSearch from "@dashboard/searches/usePageTypeSearch"; import usePageTypeSearch from "@dashboard/searches/usePageTypeSearch";
import { ListViews } from "@dashboard/types"; import { ListViews } from "@dashboard/types";
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
import createFilterHandlers from "@dashboard/utils/handlers/filterHandlers";
import createSortHandler from "@dashboard/utils/handlers/sortHandler"; import createSortHandler from "@dashboard/utils/handlers/sortHandler";
import { mapEdgesToItems, mapNodeToChoice } from "@dashboard/utils/maps"; import { mapEdgesToItems, mapNodeToChoice } from "@dashboard/utils/maps";
import { getSortParams } from "@dashboard/utils/sort"; import { getSortParams } from "@dashboard/utils/sort";
import { DialogContentText } from "@material-ui/core"; import { DialogContentText } from "@material-ui/core";
import { DeleteIcon, IconButton } from "@saleor/macaw-ui"; import isEqual from "lodash/isEqual";
import React from "react"; import React, { useCallback } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import PageListPage from "../../components/PageListPage/PageListPage"; import PageListPage from "../../components/PageListPage/PageListPage";
@ -36,6 +42,7 @@ import {
PageListUrlDialog, PageListUrlDialog,
PageListUrlQueryParams, PageListUrlQueryParams,
} from "../../urls"; } from "../../urls";
import { getFilterOpts, getFilterQueryParam, storageUtils } from "./filters";
import { getFilterVariables, getSortQueryVariables } from "./sort"; import { getFilterVariables, getSortQueryVariables } from "./sort";
interface PageListProps { interface PageListProps {
@ -45,16 +52,46 @@ interface PageListProps {
export const PageList: React.FC<PageListProps> = ({ params }) => { export const PageList: React.FC<PageListProps> = ({ params }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( const intl = useIntl();
params.ids,
);
const { updateListSettings, settings } = useListSettings( const { updateListSettings, settings } = useListSettings(
ListViews.PAGES_LIST, ListViews.PAGES_LIST,
); );
usePaginationReset(pageListUrl, params, settings.rowNumber); usePaginationReset(pageListUrl, params, settings.rowNumber);
const intl = useIntl(); const {
clearRowSelection,
selectedRowIds,
setClearDatagridRowSelectionCallback,
setSelectedRowIds,
} = useRowSelection(params);
const [changeFilters, resetFilters, handleSearchChange] =
createFilterHandlers({
cleanupFn: clearRowSelection,
createUrl: pageListUrl,
getFilterQueryParam,
navigate,
params,
keepActiveTab: true,
});
const {
selectedPreset,
presets,
hasPresetsChanged,
onPresetChange,
onPresetDelete,
onPresetSave,
onPresetUpdate,
setPresetIdToDelete,
presetIdToDelete,
} = useFilterPresets({
params,
reset: clearRowSelection,
getUrl: pageListUrl,
storageUtils,
});
const paginationState = createPaginationState(settings.rowNumber, params); const paginationState = createPaginationState(settings.rowNumber, params);
const queryVariables = React.useMemo( const queryVariables = React.useMemo(
@ -70,8 +107,10 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
variables: queryVariables, variables: queryVariables,
}); });
const pages = mapEdgesToItems(data?.pages);
const paginationValues = usePaginator({ const paginationValues = usePaginator({
pageInfo: maybe(() => data.pages.pageInfo), pageInfo: data?.pages?.pageInfo,
paginationState, paginationState,
queryString: params, queryString: params,
}); });
@ -83,7 +122,7 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
const [bulkPageRemove, bulkPageRemoveOpts] = usePageBulkRemoveMutation({ const [bulkPageRemove, bulkPageRemoveOpts] = usePageBulkRemoveMutation({
onCompleted: data => { onCompleted: data => {
if (data.pageBulkDelete.errors.length === 0) { if (data.pageBulkDelete?.errors.length === 0) {
closeModal(); closeModal();
notify({ notify({
status: "success", status: "success",
@ -93,7 +132,7 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
description: "notification", description: "notification",
}), }),
}); });
reset(); clearRowSelection();
refetch(); refetch();
} }
}, },
@ -101,7 +140,7 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
const [bulkPagePublish, bulkPagePublishOpts] = usePageBulkPublishMutation({ const [bulkPagePublish, bulkPagePublishOpts] = usePageBulkPublishMutation({
onCompleted: data => { onCompleted: data => {
if (data.pageBulkPublish.errors.length === 0) { if (data.pageBulkPublish?.errors.length === 0) {
closeModal(); closeModal();
notify({ notify({
status: "success", status: "success",
@ -111,7 +150,7 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
description: "notification", description: "notification",
}), }),
}); });
reset(); clearRowSelection();
refetch(); refetch();
} }
}, },
@ -133,66 +172,72 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
onFetchMore: loadMoreDialogPageTypes, onFetchMore: loadMoreDialogPageTypes,
}; };
const filterOpts = getFilterOpts({
params,
pageTypes: mapEdgesToItems(searchDialogPageTypesOpts?.data?.search),
pageTypesProps: {
...getSearchFetchMoreProps(
searchDialogPageTypesOpts,
loadMoreDialogPageTypes,
),
onSearchChange: searchDialogPageTypes,
},
});
const handleSetSelectedPageIds = useCallback(
(rows: number[], clearSelection: () => void) => {
if (!pages) {
return;
}
const rowsIds = rows.map(row => pages[row].id);
const haveSaveValues = isEqual(rowsIds, selectedRowIds);
if (!haveSaveValues) {
setSelectedRowIds(rowsIds);
}
setClearDatagridRowSelectionCallback(clearSelection);
},
[
pages,
selectedRowIds,
setClearDatagridRowSelectionCallback,
setSelectedRowIds,
],
);
return ( return (
<PaginatorContext.Provider value={paginationValues}> <PaginatorContext.Provider value={paginationValues}>
<PageListPage <PageListPage
disabled={loading} disabled={loading}
loading={loading}
settings={settings} settings={settings}
pages={mapEdgesToItems(data?.pages)} pages={pages}
onUpdateListSettings={updateListSettings} onUpdateListSettings={updateListSettings}
onAdd={() => openModal("create-page")} onPageCreate={() => openModal("create-page")}
onSort={handleSort} onSort={handleSort}
actionDialogOpts={{
open: openModal,
close: closeModal,
}}
params={params}
toolbar={
<>
<Button
onClick={() =>
openModal("unpublish", {
ids: listElements,
})
}
>
<FormattedMessage
id="F8gsds"
defaultMessage="Unpublish"
description="unpublish page, button"
/>
</Button>
<Button
onClick={() =>
openModal("publish", {
ids: listElements,
})
}
>
<FormattedMessage
id="yEmwxD"
defaultMessage="Publish"
description="publish page, button"
/>
</Button>
<IconButton
variant="secondary"
color="primary"
onClick={() =>
openModal("remove", {
ids: listElements,
})
}
>
<DeleteIcon />
</IconButton>
</>
}
isChecked={isSelected}
selected={listElements.length}
sort={getSortParams(params)} sort={getSortParams(params)}
toggle={toggle} selectedPageIds={selectedRowIds}
toggleAll={toggleAll} onPagesDelete={() => openModal("remove", { ids: selectedRowIds })}
onPagesPublish={() => openModal("publish", { ids: selectedRowIds })}
onPagesUnpublish={() => openModal("unpublish", { ids: selectedRowIds })}
onSelectPageIds={handleSetSelectedPageIds}
filterOpts={filterOpts}
onFilterChange={changeFilters}
initialSearch={params?.query ?? ""}
onSearchChange={handleSearchChange}
onFilterPresetChange={onPresetChange}
onFilterPresetDelete={(id: number) => {
setPresetIdToDelete(id);
openModal("delete-search");
}}
onFilterPresetUpdate={onPresetUpdate}
onFilterPresetPresetSave={() => openModal("save-search")}
selectedFilterPreset={selectedPreset}
filterPresets={presets.map(preset => preset.name)}
hasPresetsChanged={hasPresetsChanged}
onFilterPresetsAll={resetFilters}
/> />
<ActionDialog <ActionDialog
open={params.action === "publish"} open={params.action === "publish"}
@ -201,7 +246,7 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
onConfirm={() => onConfirm={() =>
bulkPagePublish({ bulkPagePublish({
variables: { variables: {
ids: params.ids, ids: selectedRowIds,
isPublished: true, isPublished: true,
}, },
}) })
@ -218,10 +263,8 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
defaultMessage="{counter,plural,one{Are you sure you want to publish this page?} other{Are you sure you want to publish {displayQuantity} pages?}}" defaultMessage="{counter,plural,one{Are you sure you want to publish this page?} other{Are you sure you want to publish {displayQuantity} pages?}}"
description="dialog content" description="dialog content"
values={{ values={{
counter: maybe(() => params.ids.length), counter: selectedRowIds.length,
displayQuantity: ( displayQuantity: <strong>{selectedRowIds.length}</strong>,
<strong>{maybe(() => params.ids.length)}</strong>
),
}} }}
/> />
</DialogContentText> </DialogContentText>
@ -233,7 +276,7 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
onConfirm={() => onConfirm={() =>
bulkPagePublish({ bulkPagePublish({
variables: { variables: {
ids: params.ids, ids: selectedRowIds,
isPublished: false, isPublished: false,
}, },
}) })
@ -249,8 +292,8 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
defaultMessage="{counter,plural,one{Are you sure you want to unpublish this page?} other{Are you sure you want to unpublish {displayQuantity} pages?}}" defaultMessage="{counter,plural,one{Are you sure you want to unpublish this page?} other{Are you sure you want to unpublish {displayQuantity} pages?}}"
description="dialog content" description="dialog content"
values={{ values={{
counter: maybe(() => params.ids.length), counter: selectedRowIds.length,
displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>, displayQuantity: <strong>{selectedRowIds.length}</strong>,
}} }}
/> />
</ActionDialog> </ActionDialog>
@ -261,7 +304,7 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
onConfirm={() => onConfirm={() =>
bulkPageRemove({ bulkPageRemove({
variables: { variables: {
ids: params.ids, ids: selectedRowIds,
}, },
}) })
} }
@ -277,8 +320,8 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
defaultMessage="{counter,plural,one{Are you sure you want to delete this page?} other{Are you sure you want to delete {displayQuantity} pages?}}" defaultMessage="{counter,plural,one{Are you sure you want to delete this page?} other{Are you sure you want to delete {displayQuantity} pages?}}"
description="dialog content" description="dialog content"
values={{ values={{
counter: maybe(() => params.ids.length), counter: selectedRowIds.length,
displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>, displayQuantity: <strong>{selectedRowIds.length}</strong>,
}} }}
/> />
</ActionDialog> </ActionDialog>
@ -299,6 +342,19 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
) )
} }
/> />
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={onPresetSave}
/>
<DeleteFilterTabDialog
open={params.action === "delete-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={onPresetDelete}
tabName={getPresetNameToDelete(presets, presetIdToDelete)}
/>
</PaginatorContext.Provider> </PaginatorContext.Provider>
); );
}; };

View file

@ -113,8 +113,7 @@ export type PageListUrlQueryParams = Pagination &
ActiveTab & ActiveTab &
Search; Search;
export const { deleteFilterTab, getFilterTabs, saveFilterTab } = export const storageUtils = createFilterTabUtils<string>(PAGES_FILTERS_KEY);
createFilterTabUtils<PageListUrlFilters>(PAGES_FILTERS_KEY);
export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } = export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
createFilterUtils<PageListUrlQueryParams, PageListUrlFilters>( createFilterUtils<PageListUrlQueryParams, PageListUrlFilters>(