Collections listing datagrid (#3835)

Co-authored-by: Krzysztof Żuraw <9116238+krzysztofzuraw@users.noreply.github.com>
Co-authored-by: wojteknowacki <124166231+wojteknowacki@users.noreply.github.com>
This commit is contained in:
Paweł Chyła 2023-07-11 08:59:01 +02:00 committed by GitHub
parent 03d9e92c97
commit 4ad8c15366
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 921 additions and 452 deletions

View file

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

View file

@ -241,34 +241,7 @@ describe("As an admin I want to manage categories", () => {
name: secondCategoryName, name: secondCategoryName,
}).then(() => { }).then(() => {
cy.visit(urlList.categories).searchInTable(startsWith); cy.visit(urlList.categories).searchInTable(startsWith);
ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable); cy.deleteTwoFirstRecordsFromGridListAndValidate("CategoryBulkDelete");
cy.get(SHARED_ELEMENTS.firstRowDataGrid)
.invoke("text")
.then(firstOnListCategoryName => {
cy.get(SHARED_ELEMENTS.secondRowDataGrid)
.invoke("text")
.then(secondOnListCategoryName => {
// deletes two first rows from categories list view
cy.clickGridCell(0, 0);
cy.clickGridCell(0, 1);
cy.get(CATEGORY_DETAILS_SELECTORS.deleteCategoriesButton)
.click()
.get(BUTTON_SELECTORS.submit)
.click()
.waitForRequestAndCheckIfNoErrors("@CategoryBulkDelete");
ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable);
cy.contains(
SHARED_ELEMENTS.dataGridTable,
firstOnListCategoryName,
).should("not.exist");
cy.contains(
SHARED_ELEMENTS.dataGridTable,
secondOnListCategoryName,
).should("not.exist");
});
});
}); });
}, },
); );

View file

@ -3,8 +3,7 @@
import faker from "faker"; import faker from "faker";
import { collectionRow } from "../../elements/catalog/collection-selectors"; import { BUTTON_SELECTORS } from "../../elements";
import { BUTTON_SELECTORS } from "../../elements/shared/button-selectors";
import { collectionDetailsUrl, urlList } from "../../fixtures/urlList"; import { collectionDetailsUrl, urlList } from "../../fixtures/urlList";
import { createChannel } from "../../support/api/requests/Channels"; import { createChannel } from "../../support/api/requests/Channels";
import { import {
@ -87,7 +86,7 @@ describe("As an admin I want to manage collections.", () => {
const collectionName = `${startsWith}${faker.datatype.number()}`; const collectionName = `${startsWith}${faker.datatype.number()}`;
let collection; let collection;
cy.visit(urlList.collections).expectSkeletonIsVisible(); cy.visit(urlList.collections);
createCollection(collectionName, false, defaultChannel).then( createCollection(collectionName, false, defaultChannel).then(
collectionResp => { collectionResp => {
collection = collectionResp; collection = collectionResp;
@ -111,7 +110,7 @@ describe("As an admin I want to manage collections.", () => {
const collectionName = `${startsWith}${faker.datatype.number()}`; const collectionName = `${startsWith}${faker.datatype.number()}`;
let collection; let collection;
cy.visit(urlList.collections).expectSkeletonIsVisible(); cy.visit(urlList.collections);
createCollection(collectionName, true, defaultChannel).then( createCollection(collectionName, true, defaultChannel).then(
collectionResp => { collectionResp => {
collection = collectionResp; collection = collectionResp;
@ -140,7 +139,7 @@ describe("As an admin I want to manage collections.", () => {
channel = channelResp; channel = channelResp;
updateChannelInProduct(product.id, channel.id); updateChannelInProduct(product.id, channel.id);
cy.visit(urlList.collections).expectSkeletonIsVisible(); cy.visit(urlList.collections);
createCollection(collectionName, false, channel).then( createCollection(collectionName, false, channel).then(
collectionResp => { collectionResp => {
collection = collectionResp; collection = collectionResp;
@ -179,7 +178,7 @@ describe("As an admin I want to manage collections.", () => {
}) })
.then(({ product: productResp }) => (createdProduct = productResp)); .then(({ product: productResp }) => (createdProduct = productResp));
cy.visit(urlList.collections).expectSkeletonIsVisible(); cy.visit(urlList.collections);
createCollection(collectionName, true, defaultChannel).then( createCollection(collectionName, true, defaultChannel).then(
collectionResp => { collectionResp => {
collection = collectionResp; collection = collectionResp;
@ -362,27 +361,14 @@ describe("As an admin I want to manage collections.", () => {
const secondCollectionName = `${deleteSeveral}${startsWith}${faker.datatype.number()}`; const secondCollectionName = `${deleteSeveral}${startsWith}${faker.datatype.number()}`;
let firstCollection; let firstCollection;
let secondCollection; let secondCollection;
cy.addAliasToGraphRequest("CollectionBulkDelete");
createCollectionRequest(firstCollectionName).then(collectionResp => { createCollectionRequest(firstCollectionName).then(collectionResp => {
firstCollection = collectionResp; firstCollection = collectionResp;
}); });
createCollectionRequest(secondCollectionName).then(collectionResp => { createCollectionRequest(secondCollectionName).then(collectionResp => {
secondCollection = collectionResp; secondCollection = collectionResp;
cy.visit(urlList.collections);
cy.visit(urlList.collections) cy.deleteTwoFirstRecordsFromGridListAndValidate("CollectionBulkDelete");
.searchInTable(deleteSeveral)
.get(collectionRow(firstCollection.id))
.find(BUTTON_SELECTORS.checkbox)
.click()
.get(collectionRow(secondCollection.id))
.find(BUTTON_SELECTORS.checkbox)
.click()
.get(BUTTON_SELECTORS.deleteIcon)
.click()
.addAliasToGraphRequest("CollectionBulkDelete")
.get(BUTTON_SELECTORS.submit)
.click()
.waitForRequestAndCheckIfNoErrors("@CollectionBulkDelete");
getCollection({ collectionId: firstCollection.id }) getCollection({ collectionId: firstCollection.id })
.its("collection") .its("collection")
.should("be.null"); .should("be.null");

View file

@ -18,8 +18,13 @@ import "@percy/cypress";
import { commandTimings } from "cypress-timings"; import { commandTimings } from "cypress-timings";
import { SHARED_ELEMENTS } from "../elements/shared/sharedElements"; import {
BUTTON_SELECTORS,
CATEGORY_DETAILS_SELECTORS,
SHARED_ELEMENTS,
} from "../elements";
import { urlList } from "../fixtures/urlList"; import { urlList } from "../fixtures/urlList";
import { ensureCanvasStatic } from "../support/customCommands/sharedElementsOperations/canvas";
import cypressGrep from "../support/cypress-grep/support"; import cypressGrep from "../support/cypress-grep/support";
commandTimings(); commandTimings();
@ -121,6 +126,39 @@ Cypress.Commands.add("clickGridHeader", col => {
cy.get("body").click(headerXCenter, headerYCenter); cy.get("body").click(headerXCenter, headerYCenter);
}); });
}); });
Cypress.Commands.add(
"deleteTwoFirstRecordsFromGridListAndValidate",
deleteRequestName => {
ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable);
cy.get(SHARED_ELEMENTS.firstRowDataGrid)
.invoke("text")
.then(firstOnListCollectionName => {
cy.get(SHARED_ELEMENTS.secondRowDataGrid)
.invoke("text")
.then(secondOnListCollectionName => {
// check two first rows on list view
cy.clickGridCell(0, 0);
cy.clickGridCell(0, 1);
cy.get(CATEGORY_DETAILS_SELECTORS.deleteCategoriesButton)
.click()
.get(BUTTON_SELECTORS.submit)
.click()
.waitForRequestAndCheckIfNoErrors(`@${deleteRequestName}`);
ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable);
cy.contains(
SHARED_ELEMENTS.dataGridTable,
firstOnListCollectionName,
).should("not.exist");
cy.contains(
SHARED_ELEMENTS.dataGridTable,
secondOnListCollectionName,
).should("not.exist");
});
});
},
);
Cypress.on( Cypress.on(
"uncaught:exception", "uncaught:exception",

View file

@ -2377,6 +2377,9 @@
"context": "option title", "context": "option title",
"string": "Prioritize warehouses by sorting order" "string": "Prioritize warehouses by sorting order"
}, },
"FTYkgw": {
"string": "Delete collections"
},
"FWbv/u": { "FWbv/u": {
"context": "page header", "context": "page header",
"string": "Create Discount" "string": "Create Discount"
@ -2397,10 +2400,6 @@
"context": "postal codes, header", "context": "postal codes, header",
"string": "Postal codes" "string": "Postal codes"
}, },
"FcVEpe": {
"context": "collection publication date",
"string": "Unpublished"
},
"FemBUF": { "FemBUF": {
"context": "header", "context": "header",
"string": "Translations to {language}" "string": "Translations to {language}"
@ -3390,10 +3389,6 @@
"context": "customers section name", "context": "customers section name",
"string": "Customers" "string": "Customers"
}, },
"Mee46w": {
"context": "collection publication date",
"string": "Becomes published on {date}"
},
"MewrtN": { "MewrtN": {
"context": "section header", "context": "section header",
"string": "Fulfillment" "string": "Fulfillment"
@ -5848,10 +5843,6 @@
"dnbJKr": { "dnbJKr": {
"string": "This transaction doesn't have any events" "string": "This transaction doesn't have any events"
}, },
"dpY94C": {
"context": "collection publication date",
"string": "Published on {date}"
},
"dsJ+Wv": { "dsJ+Wv": {
"context": "note on export gift cards", "context": "note on export gift cards",
"string": "Note: Only active and not used gift cards will be expored" "string": "Note: Only active and not used gift cards will be expored"
@ -5910,6 +5901,9 @@
"context": "button", "context": "button",
"string": "Done" "string": "Done"
}, },
"eRqx44": {
"string": "Search collections..."
},
"eUjFjW": { "eUjFjW": {
"string": "Permission group created" "string": "Permission group created"
}, },
@ -7691,9 +7685,6 @@
"context": "hide error log label in notification", "context": "hide error log label in notification",
"string": "Hide log" "string": "Hide log"
}, },
"s97tLq": {
"string": "Search Collections"
},
"s9sOcC": { "s9sOcC": {
"context": "button", "context": "button",
"string": "OK" "string": "OK"

View file

@ -1,232 +0,0 @@
// @ts-strict-ignore
import {
CollectionListUrlSortField,
collectionUrl,
} from "@dashboard/collections/urls";
import { canBeSorted } from "@dashboard/collections/views/CollectionList/sort";
import { ChannelsAvailabilityDropdown } from "@dashboard/components/ChannelsAvailabilityDropdown";
import {
getChannelAvailabilityColor,
getChannelAvailabilityLabel,
} from "@dashboard/components/ChannelsAvailabilityDropdown/utils";
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 TooltipTableCellHeader from "@dashboard/components/TooltipTableCellHeader";
import { commonTooltipMessages } from "@dashboard/components/TooltipTableCellHeader/messages";
import { CollectionListQuery } from "@dashboard/graphql";
import { maybe, renderCollection } from "@dashboard/misc";
import {
ChannelProps,
ListActions,
ListProps,
RelayToFlat,
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";
const useStyles = makeStyles(
theme => ({
[theme.breakpoints.up("lg")]: {
colAvailability: {
width: 240,
},
colName: {
paddingLeft: 0,
},
colProducts: {
width: 240,
},
},
colAvailability: {},
colName: {},
colProducts: {
textAlign: "center",
},
tableRow: {
cursor: "pointer" as "pointer",
},
}),
{ name: "CollectionList" },
);
export interface CollectionListProps
extends ListProps,
ListActions,
SortPage<CollectionListUrlSortField>,
ChannelProps {
collections: RelayToFlat<CollectionListQuery["collections"]>;
}
const numberOfColumns = 4;
const CollectionList: React.FC<CollectionListProps> = props => {
const {
collections,
disabled,
settings,
sort,
onUpdateListSettings,
onSort,
isChecked,
selected,
selectedChannelId,
toggle,
toggleAll,
toolbar,
filterDependency,
} = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<ResponsiveTable>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={collections}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCellHeader
direction={
sort.sort === CollectionListUrlSortField.name
? getArrowDirection(sort.asc)
: undefined
}
arrowPosition="right"
onClick={() => onSort(CollectionListUrlSortField.name)}
className={classes.colName}
>
<FormattedMessage id="VZsE96" defaultMessage="Collection Name" />
</TableCellHeader>
<TableCellHeader
direction={
sort.sort === CollectionListUrlSortField.productCount
? getArrowDirection(sort.asc)
: undefined
}
onClick={() => onSort(CollectionListUrlSortField.productCount)}
className={classes.colProducts}
>
<FormattedMessage id="mWQt3s" defaultMessage="No. of Products" />
</TableCellHeader>
<TooltipTableCellHeader
direction={
sort.sort === CollectionListUrlSortField.available
? getArrowDirection(sort.asc)
: undefined
}
onClick={() => onSort(CollectionListUrlSortField.available)}
className={classes.colAvailability}
disabled={
!canBeSorted(
CollectionListUrlSortField.available,
!!selectedChannelId,
)
}
tooltip={intl.formatMessage(commonTooltipMessages.noFilterSelected, {
filterName: filterDependency.label,
})}
>
<FormattedMessage
id="UxdBmI"
defaultMessage="Availability"
description="collection availability"
/>
</TooltipTableCellHeader>
</TableHead>
<TableFooter>
<TableRowLink>
<TablePaginationWithContext
colSpan={numberOfColumns}
settings={settings}
onUpdateListSettings={onUpdateListSettings}
/>
</TableRowLink>
</TableFooter>
<TableBody>
{renderCollection(
collections,
collection => {
const isSelected = collection ? isChecked(collection.id) : false;
const channel = collection?.channelListings?.find(
listing => listing?.channel?.id === selectedChannelId,
);
return (
<TableRowLink
className={classes.tableRow}
hover={!!collection}
href={collection && collectionUrl(collection.id)}
key={collection ? collection.id : "skeleton"}
selected={isSelected}
data-test-id={"id-" + maybe(() => collection.id)}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(collection.id)}
/>
</TableCell>
<TableCell className={classes.colName} data-test-id="name">
{maybe<React.ReactNode>(() => collection.name, <Skeleton />)}
</TableCell>
<TableCell className={classes.colProducts}>
{maybe<React.ReactNode>(
() => collection.products.totalCount,
<Skeleton />,
)}
</TableCell>
<TableCell
className={classes.colAvailability}
data-test-id="availability"
data-test-availability={!!collection?.channelListings?.length}
>
{(!collection && <Skeleton />) ||
(channel ? (
<Pill
label={intl.formatMessage(
getChannelAvailabilityLabel(channel),
)}
color={getChannelAvailabilityColor(channel)}
/>
) : (
<ChannelsAvailabilityDropdown
channels={collection?.channelListings}
/>
))}
</TableCell>
</TableRowLink>
);
},
() => (
<TableRowLink>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage
id="Yw+9F7"
defaultMessage="No collections found"
/>
</TableCell>
</TableRowLink>
),
)}
</TableBody>
</ResponsiveTable>
);
};
CollectionList.displayName = "CollectionList";
export default CollectionList;

View file

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

View file

@ -1,19 +0,0 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
published: {
id: "dpY94C",
defaultMessage: "Published on {date}",
description: "collection publication date",
},
unpublished: {
id: "FcVEpe",
defaultMessage: "Unpublished",
description: "collection publication date",
},
willBePublished: {
id: "Mee46w",
defaultMessage: "Becomes published on {date}",
description: "collection publication date",
},
});

View file

@ -0,0 +1,204 @@
import { Collection, Collections } from "@dashboard/collections/types";
import { CollectionListUrlSortField } from "@dashboard/collections/urls";
import { canBeSorted } from "@dashboard/collections/views/CollectionList/sort";
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 { commonTooltipMessages } from "@dashboard/components/TooltipTableCellHeader/messages";
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 {
collectionListStaticColumnsAdapter,
createGetCellContent,
} from "./datagrid";
import { messages } from "./messages";
interface CollectionListDatagridProps
extends ListProps,
SortPage<CollectionListUrlSortField> {
collections: Collections;
loading: boolean;
columnPickerSettings: string[];
selectedChannelId: string;
hasRowHover?: boolean;
onSelectCollectionIds: (
rowsIndex: number[],
clearSelection: () => void,
) => void;
onRowClick: (id: string) => void;
rowAnchor?: (id: string) => string;
}
export const CollectionListDatagrid = ({
collections,
sort,
loading,
settings,
onUpdateListSettings,
hasRowHover,
onRowClick,
rowAnchor,
disabled,
columnPickerSettings,
onSelectCollectionIds,
onSort,
filterDependency,
selectedChannelId,
}: CollectionListDatagridProps) => {
const intl = useIntl();
const { theme: currentTheme, themeValues } = useTheme();
const datagrid = useDatagridChangeState();
const collectionListStaticColumns = useMemo(
() => collectionListStaticColumnsAdapter(intl, sort),
[intl, sort],
);
const onColumnChange = useCallback(
(picked: string[]) => {
if (onUpdateListSettings) {
onUpdateListSettings("columns", picked.filter(Boolean));
}
},
[onUpdateListSettings],
);
const {
handlers,
visibleColumns,
staticColumns,
dynamicColumns,
selectedColumns,
columnCategories,
recentlyAddedColumn,
} = useColumns({
staticColumns: collectionListStaticColumns,
selectedColumns: settings?.columns ?? [],
onSave: onColumnChange,
});
const getCellContent = useCallback(
createGetCellContent({
collections,
intl,
columns: visibleColumns,
selectedChannelId,
currentTheme,
theme: themeValues,
}),
[
collections,
intl,
visibleColumns,
selectedChannelId,
currentTheme,
themeValues,
],
);
const handleRowClick = useCallback(
([_, row]: Item) => {
if (!onRowClick) {
return;
}
const rowData: Collection = collections[row];
onRowClick(rowData.id);
},
[onRowClick, collections],
);
const handleRowAnchor = useCallback(
([, row]: Item) => {
if (!rowAnchor) {
return "";
}
const rowData: Collection = collections[row];
return rowAnchor(rowData.id);
},
[rowAnchor, collections],
);
const handleGetColumnTooltipContent = useCallback(
(col: number): string => {
const columnName = visibleColumns[col].id as CollectionListUrlSortField;
if (canBeSorted(columnName, !!selectedChannelId)) {
return "";
}
// Sortable but requrie selected channel
return intl.formatMessage(commonTooltipMessages.noFilterSelected, {
filterName: filterDependency?.label ?? "",
});
},
[filterDependency, intl, selectedChannelId, visibleColumns],
);
const handleHeaderClick = useCallback(
(col: number) => {
const columnName = visibleColumns[col].id as CollectionListUrlSortField;
if (canBeSorted(columnName, !!selectedChannelId)) {
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={collections?.length ?? 0}
availableColumns={visibleColumns}
emptyText={intl.formatMessage(messages.empty)}
onRowSelectionChange={onSelectCollectionIds}
getCellContent={getCellContent}
getCellError={() => false}
selectionActions={() => null}
menuItems={() => []}
onRowClick={handleRowClick}
onHeaderClicked={handleHeaderClick}
rowAnchor={handleRowAnchor}
getColumnTooltipContent={handleGetColumnTooltipContent}
recentlyAddedColumn={recentlyAddedColumn}
renderColumnPicker={() => (
<ColumnPicker
staticColumns={staticColumns}
dynamicColumns={dynamicColumns}
selectedColumns={selectedColumns}
columnCategories={columnCategories}
onDynamicColumnSelect={handlers.onDynamicColumnSelect}
columnPickerSettings={columnPickerSettings}
onSave={handlers.onChange}
/>
)}
/>
<Box paddingX={6}>
<TablePaginationWithContext
component="div"
settings={settings}
disabled={disabled}
onUpdateListSettings={onUpdateListSettings}
/>
</Box>
</DatagridChangeStateContext.Provider>
);
};

View file

@ -0,0 +1,187 @@
import { Collection } from "@dashboard/collections/types";
import { CollectionChannels } from "@dashboard/components/ChannelsAvailabilityDropdown/utils";
import { COLOR_WARNING } from "@dashboard/misc";
import { ThemeTokensValues } from "@saleor/macaw-ui/next";
import { IntlShape } from "react-intl";
import {
getAvailablilityLabel,
getAvailablilityLabelWhenSelectedChannel,
} from "./datagrid";
const theme = {
colors: {
background: {
surfaceCriticalDepressed: "surfaceCriticalDepressed",
surfaceBrandDepressed: "surfaceBrandDepressed",
decorativeSurfaceSubdued2: "decorativeSurfaceSubdued2",
surfaceBrandSubdued: "surfaceBrandSubdued",
},
},
} as ThemeTokensValues;
const currentTheme = "defaultLight";
const intl = {
formatMessage: jest.fn(x => x.defaultMessage),
} as unknown as IntlShape;
describe("getAvailablilityLabelWhenSelectedChannel", () => {
it("should return published label when channel is active", () => {
// Arrange
const channel = {
__typename: "CollectionChannelListing",
channel: {
__typename: "Channel",
id: "223",
name: "Channel",
},
isPublished: true,
publicationDate: null,
} as CollectionChannels;
// Act;
const result = getAvailablilityLabelWhenSelectedChannel(
channel,
intl,
currentTheme,
theme,
);
// Assert
expect(result).toEqual({
color: "decorativeSurfaceSubdued2",
label: "Published",
});
});
it("should return unpublished label when channel is not active", () => {
// Arrange
const channel = {
__typename: "CollectionChannelListing",
channel: {
__typename: "Channel",
id: "223",
name: "Channel",
},
isPublished: false,
publicationDate: null,
} as CollectionChannels;
// Act;
const result = getAvailablilityLabelWhenSelectedChannel(
channel,
intl,
currentTheme,
theme,
);
// Assert
expect(result).toEqual({
color: "surfaceCriticalDepressed",
label: "Unpublished",
});
});
it("should return Scheduled to publish label when channel is not active but has scheduled dat", () => {
// Arrange
const channel = {
__typename: "CollectionChannelListing",
channel: {
__typename: "Channel",
id: "223",
name: "Channel",
},
isPublished: false,
publicationDate: "2021-09-09T12:00:00+00:00",
} as CollectionChannels;
// Act;
const result = getAvailablilityLabelWhenSelectedChannel(
channel,
intl,
currentTheme,
theme,
);
// Assert
expect(result).toEqual({
color: COLOR_WARNING,
label: "Scheduled to publish",
});
});
});
describe("getAvailablilityLabel", () => {
it("should return no channels label when there is not channels in collection", () => {
// Arrange
const collection = {
channelListings: [],
} as unknown as Collection;
// Act
const result = getAvailablilityLabel(collection, intl, currentTheme, theme);
// Assert
expect(result).toEqual({
color: "surfaceCriticalDepressed",
label: "No channels",
});
});
it("should return label with color when there are some channels in collection and are active", () => {
// Arrange
const collection = {
channelListings: [
{
__typename: "CollectionChannelListing",
channel: {
__typename: "Channel",
id: "223",
name: "Channel",
},
isPublished: true,
publicationDate: null,
},
],
} as unknown as Collection;
// Act
const result = getAvailablilityLabel(collection, intl, currentTheme, theme);
// Assert
expect(result).toEqual({
color: "decorativeSurfaceSubdued2",
label:
"{channelCount} {channelCount,plural, =1 {Channel} other {Channels}}",
});
});
it("should return label with error color when there are some channels in collection but are not active", () => {
// Arrange
const collection = {
channelListings: [
{
__typename: "CollectionChannelListing",
channel: {
__typename: "Channel",
id: "223",
name: "Channel",
},
isPublished: false,
publicationDate: null,
},
],
} as unknown as Collection;
// Act
const result = getAvailablilityLabel(collection, intl, currentTheme, theme);
// Assert
expect(result).toEqual({
color: "surfaceCriticalDepressed",
label:
"{channelCount} {channelCount,plural, =1 {Channel} other {Channels}}",
});
});
});

View file

@ -0,0 +1,163 @@
import { Collection, Collections } from "@dashboard/collections/types";
import { CollectionListUrlSortField } from "@dashboard/collections/urls";
import { messages } from "@dashboard/components/ChannelsAvailabilityDropdown/messages";
import {
CollectionChannels,
getChannelAvailabilityColor,
getChannelAvailabilityLabel,
getDropdownColor,
} from "@dashboard/components/ChannelsAvailabilityDropdown/utils";
import {
readonlyTextCell,
tagsCell,
} from "@dashboard/components/Datagrid/customCells/cells";
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
import { getStatusColor } from "@dashboard/misc";
import { Sort } from "@dashboard/types";
import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon";
import { GridCell, Item } from "@glideapps/glide-data-grid";
import { DefaultTheme, ThemeTokensValues } from "@saleor/macaw-ui/next";
import { IntlShape } from "react-intl";
import { columnsMessages } from "./messages";
export const collectionListStaticColumnsAdapter = (
intl: IntlShape,
sort: Sort<CollectionListUrlSortField>,
): AvailableColumn[] =>
[
{
id: "name",
title: intl.formatMessage(columnsMessages.name),
width: 350,
},
{
id: "productCount",
title: intl.formatMessage(columnsMessages.noOfProducts),
width: 200,
},
{
id: "availability",
title: intl.formatMessage(columnsMessages.availability),
width: 200,
},
].map(column => ({
...column,
icon: getColumnSortDirectionIcon(sort, column.id),
}));
export const createGetCellContent =
({
collections,
columns,
intl,
selectedChannelId,
theme,
currentTheme,
}: {
collections: Collections;
columns: AvailableColumn[];
intl: IntlShape;
selectedChannelId: string;
theme: ThemeTokensValues;
currentTheme: DefaultTheme;
}) =>
([column, row]: Item): GridCell => {
const rowData = collections[row];
const columnId = columns[column]?.id;
if (!columnId || !rowData) {
return readonlyTextCell("");
}
const channel = rowData.channelListings?.find(
(listing: CollectionChannels) => listing.channel.id === selectedChannelId,
);
switch (columnId) {
case "name":
return readonlyTextCell(rowData.name);
case "productCount":
return readonlyTextCell(
rowData?.products?.totalCount?.toString() ?? "",
);
case "availability":
const { label, color } = !!channel
? getAvailablilityLabelWhenSelectedChannel(
channel,
intl,
currentTheme,
theme,
)
: getAvailablilityLabel(rowData, intl, currentTheme, theme);
return tagsCell(
[
{
tag: label,
color,
},
],
[label],
{
readonly: true,
allowOverlay: false,
},
);
default:
return readonlyTextCell("");
}
};
export function getAvailablilityLabelWhenSelectedChannel(
channel: CollectionChannels,
intl: IntlShape,
currentTheme: DefaultTheme,
theme: ThemeTokensValues,
) {
const color = getStatusColor(
getChannelAvailabilityColor(channel),
currentTheme,
);
return {
label: intl.formatMessage(getChannelAvailabilityLabel(channel)),
color: getTagCellColor(color, theme),
};
}
export function getAvailablilityLabel(
rowData: Collection,
intl: IntlShape,
currentTheme: DefaultTheme,
theme: ThemeTokensValues,
) {
const availablilityLabel = rowData?.channelListings?.length
? intl.formatMessage(messages.dropdownLabel, {
channelCount: rowData?.channelListings?.length,
})
: intl.formatMessage(messages.noChannels);
const availablilityLabelColor = getStatusColor(
getDropdownColor(rowData?.channelListings || []),
currentTheme,
);
return {
label: availablilityLabel,
color: getTagCellColor(availablilityLabelColor, theme),
};
}
function getTagCellColor(
color: string,
currentTheme: ThemeTokensValues,
): string {
if (color.startsWith("#")) {
return color;
}
return currentTheme.colors.background[
color as keyof ThemeTokensValues["colors"]["background"]
];
}

View file

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

View file

@ -0,0 +1,24 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
empty: {
id: "Yw+9F7",
defaultMessage: "No collections found",
},
});
export const columnsMessages = defineMessages({
name: {
id: "VZsE96",
defaultMessage: "Collection Name",
},
noOfProducts: {
id: "mWQt3s",
defaultMessage: "No. of Products",
},
availability: {
id: "UxdBmI",
defaultMessage: "Availability",
description: "collection availability",
},
});

View file

@ -0,0 +1,40 @@
import { Button, Tooltip, TrashBinIcon } from "@saleor/macaw-ui/next";
import React, { forwardRef, ReactNode, useState } from "react";
interface CategoryDeleteButtonProps {
onClick: () => void;
children: ReactNode;
}
export const CollectionListDeleteButton = forwardRef<
HTMLButtonElement,
CategoryDeleteButtonProps
>(({ onClick, children }, ref) => {
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
return (
<Tooltip open={isTooltipOpen}>
<Tooltip.Trigger>
<Button
ref={ref}
onMouseOver={() => {
setIsTooltipOpen(true);
}}
onMouseLeave={() => {
setIsTooltipOpen(false);
}}
onClick={onClick}
icon={<TrashBinIcon />}
variant="secondary"
data-test-id="delete-categories-button"
/>
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<Tooltip.Arrow />
{children}
</Tooltip.Content>
</Tooltip>
);
});
CollectionListDeleteButton.displayName = "CollectionListDeleteButton";

View file

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

View file

@ -22,6 +22,10 @@ const props: CollectionListPageProps = {
...pageListProps.default, ...pageListProps.default,
...filterPageProps, ...filterPageProps,
...sortPageProps, ...sortPageProps,
settings: {
...pageListProps.default.settings,
columns: ["name", "productCount", "availability"],
},
sort: { sort: {
...sortPageProps.sort, ...sortPageProps.sort,
sort: CollectionListUrlSortField.name, sort: CollectionListUrlSortField.name,
@ -30,6 +34,16 @@ const props: CollectionListPageProps = {
collections, collections,
selectedChannelId: "123", selectedChannelId: "123",
filterOpts: collectionListFilterOpts, filterOpts: collectionListFilterOpts,
columnPickerSettings: ["name"],
selectedCollectionIds: [],
hasPresetsChanged: () => false,
onAll: () => undefined,
onCollectionsDelete: () => undefined,
onFilterChange: () => undefined,
loading: false,
onSort: () => undefined,
onTabUpdate: () => undefined,
onSelectCollectionIds: () => undefined,
}; };
const meta: Meta<typeof CollectionListPage> = { const meta: Meta<typeof CollectionListPage> = {

View file

@ -1,24 +1,25 @@
// @ts-strict-ignore // @ts-strict-ignore
import { collectionAddUrl } from "@dashboard/collections/urls"; import { Collections } from "@dashboard/collections/types";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { Button } from "@dashboard/components/Button";
import { getByName } from "@dashboard/components/Filter/utils";
import FilterBar from "@dashboard/components/FilterBar";
import { ListPageLayout } from "@dashboard/components/Layouts";
import { sectionNames } from "@dashboard/intl";
import { import {
FilterPageProps, collectionAddUrl,
PageListProps, CollectionListUrlSortField,
SearchPageProps, collectionUrl,
TabPageProps, } from "@dashboard/collections/urls";
} from "@dashboard/types"; import { ListFilters } from "@dashboard/components/AppLayout/ListFilters";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { getByName } from "@dashboard/components/Filter/utils";
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
import { ListPageLayout } from "@dashboard/components/Layouts";
import useNavigator from "@dashboard/hooks/useNavigator";
import { sectionNames } from "@dashboard/intl";
import { FilterPageProps, PageListProps, SortPage } from "@dashboard/types";
import { Card } from "@material-ui/core"; 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 { FormattedMessage, useIntl } from "react-intl";
import CollectionList, { import { CollectionListDatagrid } from "../CollectionListDatagrid";
CollectionListProps, import { CollectionListDeleteButton } from "../CollectionListDeleteButton";
} from "../CollectionList/CollectionList";
import { import {
CollectionFilterKeys, CollectionFilterKeys,
CollectionListFilterOpts, CollectionListFilterOpts,
@ -26,10 +27,22 @@ import {
} from "./filters"; } from "./filters";
export interface CollectionListPageProps export interface CollectionListPageProps
extends PageListProps, extends PageListProps,
SearchPageProps, Omit<
TabPageProps, FilterPageProps<CollectionFilterKeys, CollectionListFilterOpts>,
FilterPageProps<CollectionFilterKeys, CollectionListFilterOpts>, "onTabDelete"
CollectionListProps {} >,
SortPage<CollectionListUrlSortField> {
onTabUpdate: (tabName: string) => void;
selectedChannelId: string;
columnPickerSettings: string[];
collections: Collections;
loading: boolean;
selectedCollectionIds: string[];
hasPresetsChanged: () => boolean;
onSelectCollectionIds: (rows: number[], clearSelection: () => void) => void;
onCollectionsDelete: () => void;
onTabDelete: (id: number) => void;
}
const CollectionListPage: React.FC<CollectionListPageProps> = ({ const CollectionListPage: React.FC<CollectionListPageProps> = ({
currentTab, currentTab,
@ -40,61 +53,113 @@ const CollectionListPage: React.FC<CollectionListPageProps> = ({
onTabChange, onTabChange,
onTabDelete, onTabDelete,
onTabSave, onTabSave,
onTabUpdate,
selectedChannelId, selectedChannelId,
tabs, tabs,
filterOpts, filterOpts,
onFilterChange, onFilterChange,
onFilterAttributeFocus, onFilterAttributeFocus,
hasPresetsChanged,
currencySymbol,
selectedCollectionIds,
onCollectionsDelete,
...listProps ...listProps
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const navigate = useNavigator();
const filterStructure = createFilterStructure(intl, filterOpts); const filterStructure = createFilterStructure(intl, filterOpts);
const [isFilterPresetOpen, setFilterPresetOpen] = useState(false);
const filterDependency = filterStructure.find(getByName("channel")); const filterDependency = filterStructure.find(getByName("channel"));
return ( return (
<ListPageLayout> <ListPageLayout>
<TopNav title={intl.formatMessage(sectionNames.collections)}> <TopNav
<Button withoutBorder
disabled={disabled} isAlignToRight={false}
variant="primary" title={intl.formatMessage(sectionNames.collections)}
href={collectionAddUrl()} >
data-test-id="create-collection" <Box
__flex={1}
display="flex"
justifyContent="space-between"
alignItems="center"
> >
<FormattedMessage <Box display="flex">
id="jyaAlB" <Box marginX={3} display="flex" alignItems="center">
defaultMessage="Create collection" <ChevronRightIcon />
description="button" </Box>
/>
</Button> <FilterPresetsSelect
presetsChanged={hasPresetsChanged()}
onSelect={onTabChange}
onRemove={onTabDelete}
onUpdate={onTabUpdate}
savedPresets={tabs}
activePreset={currentTab}
onSelectAll={onAll}
onSave={onTabSave}
isOpen={isFilterPresetOpen}
onOpenChange={setFilterPresetOpen}
selectAllLabel={intl.formatMessage({
id: "G4g5Ii",
defaultMessage: "All Collections",
description: "tab name",
})}
/>
</Box>
<Box>
<Button
disabled={disabled}
variant="primary"
onClick={() => navigate(collectionAddUrl())}
data-test-id="create-collection"
>
<FormattedMessage
id="jyaAlB"
defaultMessage="Create collection"
description="button"
/>
</Button>
</Box>
</Box>
</TopNav> </TopNav>
<Card> <Card>
<FilterBar <ListFilters
allTabLabel={intl.formatMessage({ currencySymbol={currencySymbol}
id: "G4g5Ii",
defaultMessage: "All Collections",
description: "tab name",
})}
currentTab={currentTab}
filterStructure={filterStructure}
initialSearch={initialSearch} initialSearch={initialSearch}
onAll={onAll}
onFilterChange={onFilterChange} onFilterChange={onFilterChange}
onFilterAttributeFocus={onFilterAttributeFocus} onFilterAttributeFocus={onFilterAttributeFocus}
onSearchChange={onSearchChange} onSearchChange={onSearchChange}
onTabChange={onTabChange} filterStructure={filterStructure}
onTabDelete={onTabDelete}
onTabSave={onTabSave}
searchPlaceholder={intl.formatMessage({ searchPlaceholder={intl.formatMessage({
id: "s97tLq", id: "eRqx44",
defaultMessage: "Search Collections", defaultMessage: "Search collections...",
})} })}
tabs={tabs} actions={
<Box display="flex" gap={4}>
{selectedCollectionIds.length > 0 && (
<CollectionListDeleteButton onClick={onCollectionsDelete}>
<FormattedMessage
defaultMessage="Delete collections"
id="FTYkgw"
/>
</CollectionListDeleteButton>
)}
</Box>
}
/> />
<CollectionList
<CollectionListDatagrid
disabled={disabled} disabled={disabled}
selectedChannelId={selectedChannelId} selectedChannelId={selectedChannelId}
filterDependency={filterDependency} filterDependency={filterDependency}
onRowClick={id => {
navigate(collectionUrl(id));
}}
hasRowHover={!isFilterPresetOpen}
rowAnchor={collectionUrl}
{...listProps} {...listProps}
/> />
</Card> </Card>

View file

@ -1,13 +1,12 @@
// @ts-strict-ignore // @ts-strict-ignore
import { import {
CollectionDetailsQuery, CollectionDetailsQuery,
CollectionListQuery,
CollectionPublished, CollectionPublished,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import { RelayToFlat } from "@dashboard/types";
import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json"; import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json";
import { CollectionListFilterOpts } from "./components/CollectionListPage"; import { CollectionListFilterOpts } from "./components/CollectionListPage";
import { Collections } from "./types";
const content = richTextEditorFixtures.richTextEditor; const content = richTextEditorFixtures.richTextEditor;
@ -28,7 +27,7 @@ export const collectionListFilterOpts: CollectionListFilterOpts = {
}, },
}; };
export const collections: RelayToFlat<CollectionListQuery["collections"]> = [ export const collections: Collections = [
{ {
__typename: "Collection", __typename: "Collection",
channelListings: [ channelListings: [

7
src/collections/types.ts Normal file
View file

@ -0,0 +1,7 @@
import { CollectionListQuery } from "@dashboard/graphql";
import { RelayToFlat } from "@dashboard/types";
export type Collections = RelayToFlat<
NonNullable<CollectionListQuery["collections"]>
>;
export type Collection = Collections[number];

View file

@ -24,8 +24,8 @@ export type CollectionListUrlFilters = Filters<CollectionListUrlFiltersEnum>;
export type CollectionListUrlDialog = "remove" | TabActionDialog; export type CollectionListUrlDialog = "remove" | TabActionDialog;
export enum CollectionListUrlSortField { export enum CollectionListUrlSortField {
name = "name", name = "name",
available = "available", availability = "availability",
productCount = "products", productCount = "productCount",
} }
export type CollectionListUrlSort = Sort<CollectionListUrlSortField>; export type CollectionListUrlSort = Sort<CollectionListUrlSortField>;
export type CollectionListUrlQueryParams = ActiveTab & export type CollectionListUrlQueryParams = ActiveTab &

View file

@ -1,15 +1,14 @@
// @ts-strict-ignore // @ts-strict-ignore
import ActionDialog from "@dashboard/components/ActionDialog"; import ActionDialog from "@dashboard/components/ActionDialog";
import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext"; import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext";
import { useColumnPickerSettings } from "@dashboard/components/Datagrid/ColumnPicker/useColumnPickerSettings";
import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog"; import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog";
import SaveFilterTabDialog, { import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog";
SaveFilterTabDialogFormData,
} from "@dashboard/components/SaveFilterTabDialog";
import { import {
useCollectionBulkDeleteMutation, useCollectionBulkDeleteMutation,
useCollectionListQuery, useCollectionListQuery,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import useBulkActions from "@dashboard/hooks/useBulkActions"; import { 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";
@ -18,6 +17,7 @@ import usePaginator, {
createPaginationState, createPaginationState,
PaginatorContext, PaginatorContext,
} from "@dashboard/hooks/usePaginator"; } from "@dashboard/hooks/usePaginator";
import { useRowSelection } from "@dashboard/hooks/useRowSelection";
import { commonMessages } from "@dashboard/intl"; import { commonMessages } from "@dashboard/intl";
import { maybe } from "@dashboard/misc"; import { maybe } from "@dashboard/misc";
import { ListViews } from "@dashboard/types"; import { ListViews } from "@dashboard/types";
@ -27,8 +27,8 @@ 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, { useEffect } from "react"; import React, { useCallback, useEffect } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import CollectionListPage from "../../components/CollectionListPage/CollectionListPage"; import CollectionListPage from "../../components/CollectionListPage/CollectionListPage";
@ -38,14 +38,10 @@ import {
CollectionListUrlQueryParams, CollectionListUrlQueryParams,
} from "../../urls"; } from "../../urls";
import { import {
deleteFilterTab,
getActiveFilters,
getFilterOpts, getFilterOpts,
getFilterQueryParam, getFilterQueryParam,
getFiltersCurrentTab,
getFilterTabs,
getFilterVariables, getFilterVariables,
saveFilterTab, storageUtils,
} from "./filters"; } from "./filters";
import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort"; import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort";
@ -57,22 +53,29 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const intl = useIntl(); const intl = useIntl();
const notify = useNotifier(); const notify = useNotifier();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids,
);
const { updateListSettings, settings } = useListSettings( const { updateListSettings, settings } = useListSettings(
ListViews.COLLECTION_LIST, ListViews.COLLECTION_LIST,
); );
const { columnPickerSettings } = useColumnPickerSettings("COLLECTION_LIST");
usePaginationReset(collectionListUrl, params, settings.rowNumber); usePaginationReset(collectionListUrl, params, settings.rowNumber);
const { channel } = useAppChannel(false);
const {
clearRowSelection,
selectedRowIds,
setClearDatagridRowSelectionCallback,
setSelectedRowIds,
} = useRowSelection(params);
const [changeFilters, resetFilters, handleSearchChange] = const [changeFilters, resetFilters, handleSearchChange] =
createFilterHandlers({ createFilterHandlers({
cleanupFn: reset, cleanupFn: clearRowSelection,
createUrl: collectionListUrl, createUrl: collectionListUrl,
getFilterQueryParam, getFilterQueryParam,
navigate, navigate,
params, params,
keepActiveTab: true,
}); });
const { availableChannels } = useAppChannel(false); const { availableChannels } = useAppChannel(false);
@ -83,6 +86,23 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
channel => channel.slug === params.channel, channel => channel.slug === params.channel,
); );
const {
selectedPreset,
presets,
hasPresetsChange,
onPresetChange,
onPresetDelete,
onPresetSave,
onPresetUpdate,
setPresetIdToDelete,
presetIdToDelete,
} = useFilterPresets({
params,
reset: clearRowSelection,
getUrl: collectionListUrl,
storageUtils,
});
const paginationState = createPaginationState(settings.rowNumber, params); const paginationState = createPaginationState(settings.rowNumber, params);
const queryVariables = React.useMemo( const queryVariables = React.useMemo(
() => ({ () => ({
@ -98,6 +118,8 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
variables: queryVariables, variables: queryVariables,
}); });
const collections = mapEdgesToItems(data?.collections);
const [collectionBulkDelete, collectionBulkDeleteOpts] = const [collectionBulkDelete, collectionBulkDeleteOpts] =
useCollectionBulkDeleteMutation({ useCollectionBulkDeleteMutation({
onCompleted: data => { onCompleted: data => {
@ -107,14 +129,13 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
text: intl.formatMessage(commonMessages.savedChanges), text: intl.formatMessage(commonMessages.savedChanges),
}); });
refetch(); refetch();
reset(); clearRowSelection();
closeModal(); closeModal();
} }
}, },
}); });
const filterOpts = getFilterOpts(params, channelOpts); const filterOpts = getFilterOpts(params, channelOpts);
const tabs = getFilterTabs();
useEffect(() => { useEffect(() => {
if (!canBeSorted(params.sort, !!selectedChannel)) { if (!canBeSorted(params.sort, !!selectedChannel)) {
@ -127,34 +148,11 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
} }
}, [params]); }, [params]);
const currentTab = getFiltersCurrentTab(params, tabs);
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
CollectionListUrlDialog, CollectionListUrlDialog,
CollectionListUrlQueryParams CollectionListUrlQueryParams
>(navigate, collectionListUrl, params); >(navigate, collectionListUrl, params);
const handleTabChange = (tab: number) => {
reset();
navigate(
collectionListUrl({
activeTab: tab.toString(),
...getFilterTabs()[tab - 1].data,
}),
);
};
const handleTabDelete = () => {
deleteFilterTab(currentTab);
reset();
navigate(collectionListUrl());
};
const handleTabSave = (data: SaveFilterTabDialogFormData) => {
saveFilterTab(data.name, getActiveFilters(params));
handleTabChange(tabs.length + 1);
};
const paginationValues = usePaginator({ const paginationValues = usePaginator({
pageInfo: maybe(() => data.collections.pageInfo), pageInfo: maybe(() => data.collections.pageInfo),
paginationState, paginationState,
@ -163,53 +161,75 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
const handleSort = createSortHandler(navigate, collectionListUrl, params); const handleSort = createSortHandler(navigate, collectionListUrl, params);
const handleSetSelectedCollectionIds = useCallback(
(rows: number[], clearSelection: () => void) => {
if (!collections) {
return;
}
const rowsIds = rows.map(row => collections[row].id);
const haveSaveValues = isEqual(rowsIds, selectedRowIds);
if (!haveSaveValues) {
setSelectedRowIds(rowsIds);
}
setClearDatagridRowSelectionCallback(clearSelection);
},
[
collections,
selectedRowIds,
setClearDatagridRowSelectionCallback,
setSelectedRowIds,
],
);
return ( return (
<PaginatorContext.Provider value={paginationValues}> <PaginatorContext.Provider value={paginationValues}>
<CollectionListPage <CollectionListPage
currentTab={currentTab} currentTab={selectedPreset}
currencySymbol={channel?.currencyCode}
initialSearch={params.query || ""} initialSearch={params.query || ""}
onSearchChange={handleSearchChange} onSearchChange={handleSearchChange}
onAll={resetFilters} onAll={resetFilters}
onTabChange={handleTabChange} onTabChange={onPresetChange}
onTabDelete={() => openModal("delete-search")} onTabDelete={(id: number) => {
setPresetIdToDelete(id);
openModal("delete-search");
}}
onTabSave={() => openModal("save-search")} onTabSave={() => openModal("save-search")}
tabs={tabs.map(tab => tab.name)} onTabUpdate={onPresetUpdate}
tabs={presets.map(tab => tab.name)}
loading={loading}
disabled={loading} disabled={loading}
collections={mapEdgesToItems(data?.collections)} columnPickerSettings={columnPickerSettings}
collections={collections}
settings={settings} settings={settings}
onSort={handleSort} onSort={handleSort}
onUpdateListSettings={updateListSettings} onUpdateListSettings={updateListSettings}
sort={getSortParams(params)} sort={getSortParams(params)}
toolbar={
<IconButton
variant="secondary"
color="primary"
data-test-id="delete-icon"
onClick={() =>
openModal("remove", {
ids: listElements,
})
}
>
<DeleteIcon />
</IconButton>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
selectedChannelId={selectedChannel?.id} selectedChannelId={selectedChannel?.id}
filterOpts={filterOpts} filterOpts={filterOpts}
onFilterChange={changeFilters} onFilterChange={changeFilters}
selectedCollectionIds={selectedRowIds}
onSelectCollectionIds={handleSetSelectedCollectionIds}
hasPresetsChanged={hasPresetsChange}
onCollectionsDelete={() =>
openModal("remove", {
ids: selectedRowIds,
})
}
/> />
<ActionDialog <ActionDialog
open={params.action === "remove" && maybe(() => params.ids.length > 0)} open={
params.action === "remove" && maybe(() => selectedRowIds.length > 0)
}
onClose={closeModal} onClose={closeModal}
confirmButtonState={collectionBulkDeleteOpts.status} confirmButtonState={collectionBulkDeleteOpts.status}
onConfirm={() => onConfirm={() =>
collectionBulkDelete({ collectionBulkDelete({
variables: { variables: {
ids: params.ids, ids: selectedRowIds,
}, },
}) })
} }
@ -225,9 +245,9 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
id="yT5zvU" id="yT5zvU"
defaultMessage="{counter,plural,one{Are you sure you want to delete this collection?} other{Are you sure you want to delete {displayQuantity} collections?}}" defaultMessage="{counter,plural,one{Are you sure you want to delete this collection?} other{Are you sure you want to delete {displayQuantity} collections?}}"
values={{ values={{
counter: maybe(() => params.ids.length), counter: maybe(() => selectedRowIds.length),
displayQuantity: ( displayQuantity: (
<strong>{maybe(() => params.ids.length)}</strong> <strong>{maybe(() => selectedRowIds.length)}</strong>
), ),
}} }}
/> />
@ -237,14 +257,14 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
open={params.action === "save-search"} open={params.action === "save-search"}
confirmButtonState="default" confirmButtonState="default"
onClose={closeModal} onClose={closeModal}
onSubmit={handleTabSave} onSubmit={onPresetSave}
/> />
<DeleteFilterTabDialog <DeleteFilterTabDialog
open={params.action === "delete-search"} open={params.action === "delete-search"}
confirmButtonState="default" confirmButtonState="default"
onClose={closeModal} onClose={closeModal}
onSubmit={handleTabDelete} onSubmit={onPresetDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")} tabName={maybe(() => presets[presetIdToDelete - 1].name, "...")}
/> />
</PaginatorContext.Provider> </PaginatorContext.Provider>
); );

View file

@ -73,8 +73,9 @@ export function getFilterQueryParam(
} }
} }
export const { deleteFilterTab, getFilterTabs, saveFilterTab } = export const storageUtils = createFilterTabUtils<string>(
createFilterTabUtils<CollectionListUrlFilters>(COLLECTION_FILTERS_KEY); COLLECTION_FILTERS_KEY,
);
export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } = export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
createFilterUtils<CollectionListUrlQueryParams, CollectionListUrlFilters>( createFilterUtils<CollectionListUrlQueryParams, CollectionListUrlFilters>(

View file

@ -13,7 +13,7 @@ export function canBeSorted(
case CollectionListUrlSortField.name: case CollectionListUrlSortField.name:
case CollectionListUrlSortField.productCount: case CollectionListUrlSortField.productCount:
return true; return true;
case CollectionListUrlSortField.available: case CollectionListUrlSortField.availability:
return isChannelSelected; return isChannelSelected;
default: default:
return false; return false;
@ -26,7 +26,7 @@ export function getSortQueryField(
switch (sort) { switch (sort) {
case CollectionListUrlSortField.name: case CollectionListUrlSortField.name:
return CollectionSortField.NAME; return CollectionSortField.NAME;
case CollectionListUrlSortField.available: case CollectionListUrlSortField.availability:
return CollectionSortField.AVAILABILITY; return CollectionSortField.AVAILABILITY;
case CollectionListUrlSortField.productCount: case CollectionListUrlSortField.productCount:
return CollectionSortField.PRODUCT_COUNT; return CollectionSortField.PRODUCT_COUNT;

View file

@ -19,7 +19,7 @@ import { ColumnCategory } from "./useColumns";
export interface ColumnPickerProps { export interface ColumnPickerProps {
staticColumns: AvailableColumn[]; staticColumns: AvailableColumn[];
dynamicColumns?: AvailableColumn[]; dynamicColumns?: AvailableColumn[] | null | undefined;
selectedColumns: string[]; selectedColumns: string[];
columnCategories?: ColumnCategory[]; columnCategories?: ColumnCategory[];
columnPickerSettings?: string[]; columnPickerSettings?: string[];

View file

@ -6,7 +6,7 @@ import { AvailableColumn } from "../types";
import messages from "./messages"; import messages from "./messages";
export interface ColumnPickerDynamicColumnsProps { export interface ColumnPickerDynamicColumnsProps {
dynamicColumns: AvailableColumn[] | undefined; dynamicColumns?: AvailableColumn[] | null | undefined;
setExpanded: (value: React.SetStateAction<boolean>) => void; setExpanded: (value: React.SetStateAction<boolean>) => void;
handleToggle: (id: string) => void; handleToggle: (id: string) => void;
selectedColumns: string[]; selectedColumns: string[];

View file

@ -7,7 +7,8 @@ export type DatagridViews =
| "PRODUCT_DETAILS" | "PRODUCT_DETAILS"
| "ORDER_LIST" | "ORDER_LIST"
| "ORDER_DETAILS" | "ORDER_DETAILS"
| "ORDER_DRAFT_DETAILS"; | "ORDER_DRAFT_DETAILS"
| "COLLECTION_LIST";
type DynamicColumnSettings = { type DynamicColumnSettings = {
[view in DatagridViews]: string[]; [view in DatagridViews]: string[];
@ -19,6 +20,7 @@ export const defaultDynamicColumns: DynamicColumnSettings = {
ORDER_LIST: [], ORDER_LIST: [],
ORDER_DETAILS: [], ORDER_DETAILS: [],
ORDER_DRAFT_DETAILS: [], ORDER_DRAFT_DETAILS: [],
COLLECTION_LIST: [],
}; };
export const useColumnPickerSettings = (view: DatagridViews) => { export const useColumnPickerSettings = (view: DatagridViews) => {

View file

@ -107,7 +107,7 @@ export interface DatagridProps {
rowAnchor?: (item: Item) => string; rowAnchor?: (item: Item) => string;
rowHeight?: number | ((index: number) => number); rowHeight?: number | ((index: number) => number);
actionButtonPosition?: "left" | "right"; actionButtonPosition?: "left" | "right";
recentlyAddedColumn?: string; // Enables scroll to recently added column recentlyAddedColumn?: string | null; // Enables scroll to recently added column
} }
export const Datagrid: React.FC<DatagridProps> = ({ export const Datagrid: React.FC<DatagridProps> = ({

View file

@ -75,6 +75,7 @@ export const defaultListSettings: AppListViewSettings = {
}, },
[ListViews.COLLECTION_LIST]: { [ListViews.COLLECTION_LIST]: {
rowNumber: PAGINATE_BY, rowNumber: PAGINATE_BY,
columns: ["name", "productCount", "availability"],
}, },
[ListViews.CUSTOMER_LIST]: { [ListViews.CUSTOMER_LIST]: {
rowNumber: PAGINATE_BY, rowNumber: PAGINATE_BY,

View file

@ -563,8 +563,8 @@ export const getByUnmatchingId =
export const findById = <T extends Node>(id: string, list?: T[]) => export const findById = <T extends Node>(id: string, list?: T[]) =>
list?.find(getById(id)); list?.find(getById(id));
const COLOR_WARNING = "#FBE5AC"; export const COLOR_WARNING = "#FBE5AC";
const COLOR_WARNING_DARK = "#3E2F0A"; export const COLOR_WARNING_DARK = "#3E2F0A";
type CustomWarningColor = typeof COLOR_WARNING | typeof COLOR_WARNING_DARK; type CustomWarningColor = typeof COLOR_WARNING | typeof COLOR_WARNING_DARK;
export const getStatusColor = ( export const getStatusColor = (