Introduce datagrid on attributes list view (#4040)

This commit is contained in:
Paweł Chyła 2023-08-03 10:23:13 +02:00 committed by GitHub
parent f14ba5bcfd
commit 9037c9cfd1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 491 additions and 401 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": minor
---
Introduce datagrid on attributes list view

View file

@ -554,9 +554,6 @@
"1X6HtI": {
"string": "All Categories"
},
"1div9r": {
"string": "Search Attribute"
},
"1gzck6": {
"string": "{firstName} {lastName}"
},
@ -1534,6 +1531,9 @@
"context": "section header",
"string": "Media Information"
},
"9ScmSs": {
"string": "Search attributes..."
},
"9Sz0By": {
"context": "channel currency",
"string": "Currency"
@ -2786,6 +2786,10 @@
"HvJPcU": {
"string": "Category deleted"
},
"I+1KzL": {
"context": "tab name",
"string": "All attributes"
},
"I+UwqI": {
"context": "is filter range or value",
"string": "equal to"
@ -5842,10 +5846,6 @@
"context": "vat included in order price",
"string": "VAT included"
},
"dKPMyh": {
"context": "tab name",
"string": "All Attributes"
},
"dM86a2": {
"string": "No categories found"
},
@ -6150,6 +6150,9 @@
"g/BrOt": {
"string": "Url has invalid format"
},
"g0GAdN": {
"string": "Delete attributes"
},
"g3qjSf": {
"context": "button",
"string": "Assign categories"
@ -6952,10 +6955,6 @@
"context": "order refund amount, input label",
"string": "Amount"
},
"lw9WIk": {
"context": "deleted multiple attributes",
"string": "Attributes successfully delete"
},
"lwjzVj": {
"string": "Edit order"
},
@ -8707,6 +8706,10 @@
"context": "hint",
"string": "Usually ends with /api/manifest"
},
"z3GGbZ": {
"context": "deleted multiple attributes",
"string": "Attributes successfully deleted"
},
"z8jo8h": {
"context": "button",
"string": "View products"

View file

@ -1,258 +0,0 @@
import {
AttributeListUrlSortField,
attributeUrl,
} from "@dashboard/attributes/urls";
import Checkbox from "@dashboard/components/Checkbox";
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 { AttributeFragment } from "@dashboard/graphql";
import { translateBoolean } from "@dashboard/intl";
import { renderCollection } from "@dashboard/misc";
import { ListActions, ListProps, SortPage } from "@dashboard/types";
import { getArrowDirection } from "@dashboard/utils/sort";
import { 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 AttributeListProps
extends ListProps,
ListActions,
SortPage<AttributeListUrlSortField> {
attributes: AttributeFragment[];
}
const useStyles = makeStyles(
theme => ({
[theme.breakpoints.up("lg")]: {
colFaceted: {
width: 180,
},
colName: {
width: "auto",
},
colSearchable: {
width: 180,
},
colSlug: {
width: 200,
},
colVisible: {
width: 180,
},
},
colFaceted: {
textAlign: "center",
},
colName: {},
colSearchable: {
textAlign: "center",
},
colSlug: {
paddingLeft: 0,
},
colVisible: {
textAlign: "center",
},
link: {
cursor: "pointer",
},
}),
{ name: "AttributeList" },
);
const numberOfColumns = 6;
const AttributeList: React.FC<AttributeListProps> = ({
attributes,
disabled,
isChecked,
selected,
sort,
toggle,
toggleAll,
toolbar,
onSort,
}) => {
const classes = useStyles({});
const intl = useIntl();
return (
<ResponsiveTable>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={attributes}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCellHeader
className={classes.colSlug}
direction={
sort.sort === AttributeListUrlSortField.slug
? getArrowDirection(!!sort.asc)
: undefined
}
arrowPosition="right"
onClick={() => onSort(AttributeListUrlSortField.slug)}
>
<FormattedMessage id="oJkeS6" defaultMessage="Attribute Code" />
</TableCellHeader>
<TableCellHeader
className={classes.colName}
direction={
sort.sort === AttributeListUrlSortField.name
? getArrowDirection(!!sort.asc)
: undefined
}
onClick={() => onSort(AttributeListUrlSortField.name)}
>
<FormattedMessage
id="HjUoHK"
defaultMessage="Default Label"
description="attribute's label'"
/>
</TableCellHeader>
<TableCellHeader
className={classes.colVisible}
direction={
sort.sort === AttributeListUrlSortField.visible
? getArrowDirection(!!sort.asc)
: undefined
}
textAlign="center"
onClick={() => onSort(AttributeListUrlSortField.visible)}
>
<FormattedMessage
id="k6WDZl"
defaultMessage="Visible"
description="attribute is visible"
/>
</TableCellHeader>
<TableCellHeader
className={classes.colSearchable}
direction={
sort.sort === AttributeListUrlSortField.searchable
? getArrowDirection(!!sort.asc)
: undefined
}
textAlign="center"
onClick={() => onSort(AttributeListUrlSortField.searchable)}
>
<FormattedMessage
id="yKuba7"
defaultMessage="Searchable"
description="attribute can be searched in dashboard"
/>
</TableCellHeader>
<TableCellHeader
className={classes.colFaceted}
direction={
sort.sort === AttributeListUrlSortField.useInFacetedSearch
? getArrowDirection(!!sort.asc)
: undefined
}
textAlign="center"
onClick={() => onSort(AttributeListUrlSortField.useInFacetedSearch)}
>
<FormattedMessage
defaultMessage="Use as filter"
id="Y3pCRX"
description="attribute can be searched in storefront"
/>
</TableCellHeader>
</TableHead>
<TableFooter>
<TableRowLink>
<TablePaginationWithContext colSpan={numberOfColumns} />
</TableRowLink>
</TableFooter>
<TableBody>
{renderCollection(
attributes,
attribute => {
const isSelected = attribute ? isChecked(attribute.id) : false;
return (
<TableRowLink
selected={isSelected}
hover={!!attribute}
key={attribute ? attribute.id : "skeleton"}
href={attribute && attributeUrl(attribute.id)}
className={classes.link}
data-test-id={`id-${attribute?.id}`}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(attribute?.id ?? "")}
/>
</TableCell>
<TableCell className={classes.colSlug} data-test-id="slug">
{attribute ? attribute.slug : <Skeleton />}
</TableCell>
<TableCell className={classes.colName} data-test-id="name">
{attribute ? attribute.name : <Skeleton />}
</TableCell>
<TableCell
className={classes.colVisible}
data-test-id="visible"
data-test-visible={attribute?.visibleInStorefront}
>
{attribute ? (
translateBoolean(attribute.visibleInStorefront, intl)
) : (
<Skeleton />
)}
</TableCell>
<TableCell
className={classes.colSearchable}
data-test-id="searchable"
data-test-searchable={attribute?.filterableInDashboard}
>
{attribute ? (
translateBoolean(attribute.filterableInDashboard, intl)
) : (
<Skeleton />
)}
</TableCell>
<TableCell
className={classes.colFaceted}
data-test-id="use-in-faceted-search"
data-test-use-in-faceted-search={
attribute?.filterableInStorefront
}
>
{attribute ? (
translateBoolean(attribute.filterableInStorefront, intl)
) : (
<Skeleton />
)}
</TableCell>
</TableRowLink>
);
},
() => (
<TableRowLink>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage
id="ztQgD8"
defaultMessage="No attributes found"
/>
</TableCell>
</TableRowLink>
),
)}
</TableBody>
</ResponsiveTable>
);
};
AttributeList.displayName = "AttributeList";
export default AttributeList;

View file

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

View file

@ -0,0 +1,150 @@
import {
AttributeListUrlSortField,
attributeUrl,
} from "@dashboard/attributes/urls";
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 { AttributeFragment } from "@dashboard/graphql";
import useNavigator from "@dashboard/hooks/useNavigator";
import { ListProps, SortPage } from "@dashboard/types";
import { Item } from "@glideapps/glide-data-grid";
import { Box } from "@saleor/macaw-ui/next";
import React, { useCallback, useMemo } from "react";
import { useIntl } from "react-intl";
import {
attributesListStaticColumnsAdapter,
createGetCellContent,
} from "./datagrid";
import { messages } from "./messages";
interface AttributeListDatagridProps
extends ListProps,
SortPage<AttributeListUrlSortField> {
attributes: AttributeFragment[];
onSelectAttributesIds: (
rowsIndex: number[],
clearSelection: () => void,
) => void;
}
export const AttributeListDatagrid = ({
attributes,
settings,
sort,
disabled,
onSort,
onSelectAttributesIds,
onUpdateListSettings,
}: AttributeListDatagridProps) => {
const datagridState = useDatagridChangeState();
const navigate = useNavigator();
const intl = useIntl();
const attributesListStaticColumns = useMemo(
() => attributesListStaticColumnsAdapter(intl, sort),
[intl, sort],
);
const onColumnChange = useCallback(
(picked: string[]) => {
if (onUpdateListSettings) {
onUpdateListSettings("columns", picked.filter(Boolean));
}
},
[onUpdateListSettings],
);
const {
handlers,
visibleColumns,
recentlyAddedColumn,
staticColumns,
selectedColumns,
} = useColumns({
selectedColumns: settings?.columns ?? [],
staticColumns: attributesListStaticColumns,
onSave: onColumnChange,
});
const getCellContent = useCallback(
createGetCellContent({
attributes,
columns: visibleColumns,
intl,
}),
[attributes, intl, visibleColumns],
);
const handleRowClick = useCallback(
([_, row]: Item) => {
const rowData: AttributeFragment = attributes[row];
if (rowData) {
navigate(attributeUrl(rowData.id));
}
},
[attributes],
);
const handleRowAnchor = useCallback(
([, row]: Item) => attributeUrl(attributes[row].id),
[attributes],
);
const handleHeaderClick = useCallback(
(col: number) => {
const columnName = visibleColumns[col].id as AttributeListUrlSortField;
onSort(columnName);
},
[visibleColumns, onSort],
);
return (
<DatagridChangeStateContext.Provider value={datagridState}>
<Datagrid
readonly
loading={disabled}
rowMarkers="checkbox"
columnSelect="single"
hasRowHover={true}
onColumnMoved={handlers.onMove}
onColumnResize={handlers.onResize}
verticalBorder={col => col > 0}
rows={attributes?.length ?? 0}
availableColumns={visibleColumns}
emptyText={intl.formatMessage(messages.empty)}
onRowSelectionChange={onSelectAttributesIds}
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={disabled}
onUpdateListSettings={onUpdateListSettings}
/>
</Box>
</DatagridChangeStateContext.Provider>
);
};

View file

@ -0,0 +1,87 @@
import { AttributeListUrlSortField } from "@dashboard/attributes/urls";
import { PLACEHOLDER } from "@dashboard/components/Datagrid/const";
import { readonlyTextCell } from "@dashboard/components/Datagrid/customCells/cells";
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
import { AttributeFragment } from "@dashboard/graphql";
import { translateBoolean } from "@dashboard/intl";
import { Sort } from "@dashboard/types";
import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon";
import { GridCell, Item } from "@glideapps/glide-data-grid";
import { IntlShape } from "react-intl";
import { columnsMessages } from "./messages";
export const attributesListStaticColumnsAdapter = (
intl: IntlShape,
sort: Sort<AttributeListUrlSortField>,
) =>
[
{
id: "slug",
title: intl.formatMessage(columnsMessages.slug),
width: 300,
},
{
id: "name",
title: intl.formatMessage(columnsMessages.name),
width: 300,
},
{
id: "visible",
title: intl.formatMessage(columnsMessages.visible),
width: 200,
},
{
id: "searchable",
title: intl.formatMessage(columnsMessages.searchable),
width: 200,
},
{
id: "use-in-faceted-search",
title: intl.formatMessage(columnsMessages.useInFacetedSearch),
width: 200,
},
].map(column => ({
...column,
icon: getColumnSortDirectionIcon(sort, column.id),
}));
export const createGetCellContent =
({
attributes,
columns,
intl,
}: {
attributes: AttributeFragment[];
columns: AvailableColumn[];
intl: IntlShape;
}) =>
([column, row]: Item): GridCell => {
const rowData: AttributeFragment | undefined = attributes[row];
const columnId = columns[column]?.id;
if (!columnId || !rowData) {
return readonlyTextCell("");
}
switch (columnId) {
case "slug":
return readonlyTextCell(rowData?.slug ?? PLACEHOLDER);
case "name":
return readonlyTextCell(rowData?.name ?? PLACEHOLDER);
case "visible":
return readonlyTextCell(
translateBoolean(rowData?.visibleInStorefront, intl),
);
case "searchable":
return readonlyTextCell(
translateBoolean(rowData?.filterableInDashboard, intl),
);
case "use-in-faceted-search":
return readonlyTextCell(
translateBoolean(rowData?.filterableInStorefront, intl),
);
default:
return readonlyTextCell("");
}
};

View file

@ -0,0 +1 @@
export * from "./AttributeListDatagrid";

View file

@ -0,0 +1,35 @@
import { defineMessages } from "react-intl";
export const columnsMessages = defineMessages({
slug: {
id: "oJkeS6",
defaultMessage: "Attribute Code",
},
name: {
id: "HjUoHK",
defaultMessage: "Default Label",
description: "attribute's label'",
},
visible: {
id: "k6WDZl",
defaultMessage: "Visible",
description: "attribute is visible",
},
searchable: {
id: "yKuba7",
defaultMessage: "Searchable",
description: "attribute can be searched in dashboard",
},
useInFacetedSearch: {
defaultMessage: "Use as filter",
id: "Y3pCRX",
description: "attribute can be searched in storefront",
},
});
export const messages = defineMessages({
empty: {
id: "ztQgD8",
defaultMessage: "No attributes found",
},
});

View file

@ -1,26 +1,26 @@
// @ts-strict-ignore
import {
attributeAddUrl,
AttributeListUrlSortField,
} from "@dashboard/attributes/urls";
import { ListFilters } from "@dashboard/components/AppLayout/ListFilters";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { Button } from "@dashboard/components/Button";
import FilterBar from "@dashboard/components/FilterBar";
import { BulkDeleteButton } from "@dashboard/components/BulkDeleteButton";
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
import { configurationMenuUrl } from "@dashboard/configuration";
import { AttributeFragment } from "@dashboard/graphql";
import useNavigator from "@dashboard/hooks/useNavigator";
import { sectionNames } from "@dashboard/intl";
import { Card } from "@material-ui/core";
import React from "react";
import { Box, Button, ChevronRightIcon } from "@saleor/macaw-ui/next";
import React, { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import {
FilterPageProps,
ListActions,
FilterPagePropsWithPresets,
PageListProps,
SortPage,
TabPageProps,
} from "../../../types";
import AttributeList from "../AttributeList/AttributeList";
import { AttributeListDatagrid } from "../AttributeListDatagrid";
import {
AttributeFilterKeys,
AttributeListFilterOpts,
@ -29,11 +29,12 @@ import {
export interface AttributeListPageProps
extends PageListProps,
ListActions,
FilterPageProps<AttributeFilterKeys, AttributeListFilterOpts>,
SortPage<AttributeListUrlSortField>,
TabPageProps {
FilterPagePropsWithPresets<AttributeFilterKeys, AttributeListFilterOpts>,
SortPage<AttributeListUrlSortField> {
attributes: AttributeFragment[];
selectedAttributesIds: string[];
onAttributesDelete: () => void;
onSelectAttributesIds: (rows: number[], clearSelection: () => void) => void;
}
const AttributeListPage: React.FC<AttributeListPageProps> = ({
@ -41,26 +42,65 @@ const AttributeListPage: React.FC<AttributeListPageProps> = ({
initialSearch,
onFilterChange,
onSearchChange,
currentTab,
onAll,
onTabChange,
onTabDelete,
onTabSave,
tabs,
hasPresetsChanged,
onFilterPresetChange,
onFilterPresetDelete,
onFilterPresetPresetSave,
onFilterPresetUpdate,
onFilterPresetsAll,
filterPresets,
selectedFilterPreset,
onAttributesDelete,
selectedAttributesIds,
currencySymbol,
...listProps
}) => {
const intl = useIntl();
const navigate = useNavigator();
const structure = createFilterStructure(intl, filterOpts);
const [isFilterPresetOpen, setFilterPresetOpen] = useState(false);
return (
<>
<TopNav
href={configurationMenuUrl}
title={intl.formatMessage(sectionNames.attributes)}
withoutBorder
isAlignToRight={false}
>
<Box
__flex={1}
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Box display="flex">
<Box marginX={3} 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: "I+1KzL",
defaultMessage: "All attributes",
description: "tab name",
})}
/>
</Box>
<Box>
<Button
href={attributeAddUrl()}
onClick={() => navigate(attributeAddUrl())}
variant="primary"
data-test-id="create-attribute-button"
>
@ -70,30 +110,35 @@ const AttributeListPage: React.FC<AttributeListPageProps> = ({
description="button"
/>
</Button>
</Box>
</Box>
</TopNav>
<Card>
<FilterBar
allTabLabel={intl.formatMessage({
id: "dKPMyh",
defaultMessage: "All Attributes",
description: "tab name",
})}
currentTab={currentTab}
filterStructure={structure}
<ListFilters<AttributeFilterKeys>
currencySymbol={currencySymbol}
initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage({
id: "1div9r",
defaultMessage: "Search Attribute",
})}
tabs={tabs}
onAll={onAll}
onFilterChange={onFilterChange}
onSearchChange={onSearchChange}
onTabChange={onTabChange}
onTabDelete={onTabDelete}
onTabSave={onTabSave}
filterStructure={structure}
searchPlaceholder={intl.formatMessage({
id: "9ScmSs",
defaultMessage: "Search attributes...",
})}
actions={
<Box display="flex" gap={4}>
{selectedAttributesIds.length > 0 && (
<BulkDeleteButton onClick={onAttributesDelete}>
<FormattedMessage
defaultMessage="Delete attributes"
id="g0GAdN"
/>
<AttributeList {...listProps} />
</BulkDeleteButton>
)}
</Box>
}
/>
<AttributeListDatagrid {...listProps} />
</Card>
</>
);

View file

@ -1,39 +1,34 @@
// @ts-strict-ignore
import {
deleteFilterTab,
getActiveFilters,
getFilterOpts,
getFiltersCurrentTab,
getFilterTabs,
getFilterVariables,
saveFilterTab,
storageUtils,
} from "@dashboard/attributes/views/AttributeList/filters";
import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog";
import SaveFilterTabDialog, {
SaveFilterTabDialogFormData,
} from "@dashboard/components/SaveFilterTabDialog";
import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog";
import {
useAttributeBulkDeleteMutation,
useAttributeListQuery,
} from "@dashboard/graphql";
import { useFilterPresets } from "@dashboard/hooks/useFilterPresets";
import useListSettings from "@dashboard/hooks/useListSettings";
import useNavigator from "@dashboard/hooks/useNavigator";
import useNotifier from "@dashboard/hooks/useNotifier";
import { usePaginationReset } from "@dashboard/hooks/usePaginationReset";
import usePaginator, {
createPaginationState,
PaginatorContext,
} from "@dashboard/hooks/usePaginator";
import { useRowSelection } from "@dashboard/hooks/useRowSelection";
import { ListViews } from "@dashboard/types";
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
import createFilterHandlers from "@dashboard/utils/handlers/filterHandlers";
import createSortHandler from "@dashboard/utils/handlers/sortHandler";
import { mapEdgesToItems } from "@dashboard/utils/maps";
import { getSortParams } from "@dashboard/utils/sort";
import { DeleteIcon, IconButton } from "@saleor/macaw-ui";
import React from "react";
import isEqual from "lodash/isEqual";
import React, { useCallback } from "react";
import { useIntl } from "react-intl";
import { PAGINATE_BY } from "../../../config";
import useBulkActions from "../../../hooks/useBulkActions";
import { maybe } from "../../../misc";
import AttributeBulkDeleteDialog from "../../components/AttributeBulkDeleteDialog";
import AttributeListPage from "../../components/AttributeListPage";
import {
@ -51,24 +46,52 @@ interface AttributeListProps {
const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
const navigate = useNavigator();
const notify = useNotifier();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids,
);
const intl = useIntl();
const paginationState = createPaginationState(PAGINATE_BY, params);
const { updateListSettings, settings } = useListSettings(
ListViews.ATTRIBUTE_LIST,
);
usePaginationReset(attributeListUrl, params, settings.rowNumber);
const paginationState = createPaginationState(settings.rowNumber, params);
const queryVariables = React.useMemo(
() => ({
...paginationState,
filter: getFilterVariables(params),
sort: getSortQueryVariables(params),
}),
[params],
[params, settings.rowNumber],
);
const { data, loading, refetch } = useAttributeListQuery({
variables: queryVariables,
});
const {
clearRowSelection,
selectedRowIds,
setSelectedRowIds,
setClearDatagridRowSelectionCallback,
} = useRowSelection(params);
const {
hasPresetsChanged,
onPresetChange,
onPresetDelete,
onPresetSave,
onPresetUpdate,
selectedPreset,
presets,
getPresetNameToDelete,
setPresetIdToDelete,
} = useFilterPresets({
getUrl: attributeListUrl,
params,
storageUtils,
reset: clearRowSelection,
});
const [attributeBulkDelete, attributeBulkDeleteOpts] =
useAttributeBulkDeleteMutation({
onCompleted: data => {
@ -77,21 +100,17 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
notify({
status: "success",
text: intl.formatMessage({
id: "lw9WIk",
defaultMessage: "Attributes successfully delete",
id: "z3GGbZ",
defaultMessage: "Attributes successfully deleted",
description: "deleted multiple attributes",
}),
});
reset();
clearRowSelection();
refetch();
}
},
});
const tabs = getFilterTabs();
const currentTab = getFiltersCurrentTab(params, tabs);
const [openModal, closeModal] = createDialogActionHandlers<
AttributeListUrlDialog,
AttributeListUrlQueryParams
@ -99,34 +118,14 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
const [changeFilters, resetFilters, handleSearchChange] =
createFilterHandlers({
cleanupFn: reset,
cleanupFn: clearRowSelection,
createUrl: attributeListUrl,
getFilterQueryParam,
navigate,
params,
keepActiveTab: true,
});
const handleTabChange = (tab: number) => {
reset();
navigate(
attributeListUrl({
activeTab: tab.toString(),
...getFilterTabs()[tab - 1].data,
}),
);
};
const handleTabDelete = () => {
deleteFilterTab(currentTab);
reset();
navigate(attributeListUrl());
};
const handleTabSave = (data: SaveFilterTabDialogFormData) => {
saveFilterTab(data.name, getActiveFilters(params));
handleTabChange(tabs.length + 1);
};
const paginationValues = usePaginator({
pageInfo: data?.attributes?.pageInfo,
paginationState,
@ -135,64 +134,84 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
const handleSort = createSortHandler(navigate, attributeListUrl, params);
const attributes = mapEdgesToItems(data?.attributes);
const handleSelectAttributesIds = useCallback(
(rows: number[], clearSelection: () => void) => {
if (!attributes) {
return;
}
const rowsIds = rows.map(row => attributes[row]?.id);
const haveSaveValues = isEqual(rowsIds, selectedRowIds);
if (!haveSaveValues) {
setSelectedRowIds(rowsIds);
}
setClearDatagridRowSelectionCallback(clearSelection);
},
[
attributes,
selectedRowIds,
setClearDatagridRowSelectionCallback,
setSelectedRowIds,
],
);
return (
<PaginatorContext.Provider value={paginationValues}>
<AttributeListPage
attributes={mapEdgesToItems(data?.attributes) ?? []}
currentTab={currentTab}
settings={settings}
onUpdateListSettings={updateListSettings}
onFilterPresetsAll={resetFilters}
onFilterPresetDelete={(id: number) => {
setPresetIdToDelete(id);
openModal("delete-search");
}}
onFilterPresetPresetSave={() => openModal("save-search")}
onFilterPresetChange={onPresetChange}
onFilterPresetUpdate={onPresetUpdate}
hasPresetsChanged={hasPresetsChanged}
onAttributesDelete={() => openModal("remove")}
selectedFilterPreset={selectedPreset}
selectedAttributesIds={selectedRowIds}
filterPresets={presets.map(tab => tab.name)}
attributes={attributes ?? []}
disabled={loading || attributeBulkDeleteOpts.loading}
filterOpts={getFilterOpts(params)}
initialSearch={params.query || ""}
isChecked={isSelected}
onAll={resetFilters}
onFilterChange={changeFilters}
onSearchChange={handleSearchChange}
onSort={handleSort}
onTabChange={handleTabChange}
onTabDelete={() => openModal("delete-search")}
onTabSave={() => openModal("save-search")}
selected={listElements.length}
sort={getSortParams(params)}
tabs={tabs.map(tab => tab.name)}
toggle={toggle}
toggleAll={toggleAll}
toolbar={
<IconButton
variant="secondary"
color="primary"
onClick={() =>
openModal("remove", {
ids: listElements,
})
}
>
<DeleteIcon />
</IconButton>
}
onSelectAttributesIds={handleSelectAttributesIds}
/>
<AttributeBulkDeleteDialog
confirmButtonState={attributeBulkDeleteOpts.status}
open={
params.action === "remove" && !!params.ids && params.ids.length > 0
}
onConfirm={() =>
attributeBulkDelete({ variables: { ids: params?.ids ?? [] } })
}
open={params.action === "remove" && selectedRowIds.length > 0}
onConfirm={async () => {
await attributeBulkDelete({ variables: { ids: selectedRowIds } });
clearRowSelection();
}}
onClose={closeModal}
quantity={params.ids?.length ?? 0}
quantity={selectedRowIds.length}
/>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabSave}
onSubmit={onPresetSave}
/>
<DeleteFilterTabDialog
open={params.action === "delete-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
onSubmit={onPresetDelete}
tabName={getPresetNameToDelete()}
/>
</PaginatorContext.Provider>
);

View file

@ -98,8 +98,7 @@ export function getFilterQueryParam(
}
}
export const { deleteFilterTab, getFilterTabs, saveFilterTab } =
createFilterTabUtils<AttributeListUrlFilters>(ATTRIBUTE_FILTERS_KEY);
export const storageUtils = createFilterTabUtils<string>(ATTRIBUTE_FILTERS_KEY);
export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
createFilterUtils<AttributeListUrlQueryParams, AttributeListUrlFilters>(

View file

@ -34,6 +34,7 @@ export const Root: React.FC<PropsWithChildren<TopNavProps>> = ({
<Box
display="flex"
flexWrap="nowrap"
height="100%"
__flex={isAlignToRight ? "initial" : 1}
>
{isPickerActive && (

View file

@ -42,6 +42,7 @@ export type ProductListColumns =
export interface AppListViewSettings {
[ListViews.APPS_LIST]: ListSettings;
[ListViews.ATTRIBUTE_VALUE_LIST]: ListSettings;
[ListViews.ATTRIBUTE_LIST]: ListSettings;
[ListViews.CATEGORY_LIST]: ListSettings;
[ListViews.COLLECTION_LIST]: ListSettings;
[ListViews.CUSTOMER_LIST]: ListSettings;
@ -71,6 +72,10 @@ export const defaultListSettings: AppListViewSettings = {
[ListViews.ATTRIBUTE_VALUE_LIST]: {
rowNumber: 10,
},
[ListViews.ATTRIBUTE_LIST]: {
rowNumber: 10,
columns: ["slug", "name", "visible", "searchable", "use-in-faceted-search"],
},
[ListViews.CATEGORY_LIST]: {
rowNumber: PAGINATE_BY,
columns: ["name", "products", "subcategories"],