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:
parent
03d9e92c97
commit
4ad8c15366
29 changed files with 921 additions and 452 deletions
5
.changeset/gentle-bugs-rest.md
Normal file
5
.changeset/gentle-bugs-rest.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"saleor-dashboard": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Introduce datagrid in collection list view
|
|
@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
|
|
@ -1,2 +0,0 @@
|
||||||
export { default } from "./CollectionList";
|
|
||||||
export * from "./CollectionList";
|
|
|
@ -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",
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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}}",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
163
src/collections/components/CollectionListDatagrid/datagrid.ts
Normal file
163
src/collections/components/CollectionListDatagrid/datagrid.ts
Normal 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"]
|
||||||
|
];
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./CollectionListDatagrid";
|
|
@ -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",
|
||||||
|
},
|
||||||
|
});
|
|
@ -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";
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./CollectionListDeleteButton";
|
|
@ -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> = {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
7
src/collections/types.ts
Normal 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];
|
|
@ -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 &
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>(
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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[];
|
||||||
|
|
|
@ -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[];
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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> = ({
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
Loading…
Reference in a new issue