From 4ad8c153661af6e6978bcf0f97cd1d2b54597bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Tue, 11 Jul 2023 08:59:01 +0200 Subject: [PATCH] Collections listing datagrid (#3835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Krzysztof Żuraw <9116238+krzysztofzuraw@users.noreply.github.com> Co-authored-by: wojteknowacki <124166231+wojteknowacki@users.noreply.github.com> --- .changeset/gentle-bugs-rest.md | 5 + cypress/e2e/catalog/categories.js | 29 +-- cypress/e2e/catalog/collections.js | 30 +-- cypress/support/e2e.js | 40 ++- locale/defaultMessages.json | 21 +- .../CollectionList/CollectionList.tsx | 232 ------------------ .../components/CollectionList/index.ts | 2 - .../components/CollectionList/messages.ts | 19 -- .../CollectionListDatagrid.tsx | 204 +++++++++++++++ .../CollectionListDatagrid/datagrid.test.ts | 187 ++++++++++++++ .../CollectionListDatagrid/datagrid.ts | 163 ++++++++++++ .../CollectionListDatagrid/index.ts | 1 + .../CollectionListDatagrid/messages.ts | 24 ++ .../CollectionListDeleteButton.tsx | 40 +++ .../CollectionListDeleteButton/index.ts | 1 + .../CollectionListPage.stories.tsx | 14 ++ .../CollectionListPage/CollectionListPage.tsx | 161 ++++++++---- src/collections/fixtures.ts | 5 +- src/collections/types.ts | 7 + src/collections/urls.ts | 4 +- .../views/CollectionList/CollectionList.tsx | 160 ++++++------ .../views/CollectionList/filters.ts | 5 +- src/collections/views/CollectionList/sort.ts | 4 +- .../Datagrid/ColumnPicker/ColumnPicker.tsx | 2 +- .../ColumnPickerDynamicColumns.tsx | 2 +- .../ColumnPicker/useColumnPickerSettings.ts | 4 +- src/components/Datagrid/Datagrid.tsx | 2 +- src/config.ts | 1 + src/misc.ts | 4 +- 29 files changed, 921 insertions(+), 452 deletions(-) create mode 100644 .changeset/gentle-bugs-rest.md delete mode 100644 src/collections/components/CollectionList/CollectionList.tsx delete mode 100644 src/collections/components/CollectionList/index.ts delete mode 100644 src/collections/components/CollectionList/messages.ts create mode 100644 src/collections/components/CollectionListDatagrid/CollectionListDatagrid.tsx create mode 100644 src/collections/components/CollectionListDatagrid/datagrid.test.ts create mode 100644 src/collections/components/CollectionListDatagrid/datagrid.ts create mode 100644 src/collections/components/CollectionListDatagrid/index.ts create mode 100644 src/collections/components/CollectionListDatagrid/messages.ts create mode 100644 src/collections/components/CollectionListDeleteButton/CollectionListDeleteButton.tsx create mode 100644 src/collections/components/CollectionListDeleteButton/index.ts create mode 100644 src/collections/types.ts diff --git a/.changeset/gentle-bugs-rest.md b/.changeset/gentle-bugs-rest.md new file mode 100644 index 000000000..6d09d49a0 --- /dev/null +++ b/.changeset/gentle-bugs-rest.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Introduce datagrid in collection list view diff --git a/cypress/e2e/catalog/categories.js b/cypress/e2e/catalog/categories.js index 13af27142..c5cdef1b5 100644 --- a/cypress/e2e/catalog/categories.js +++ b/cypress/e2e/catalog/categories.js @@ -241,34 +241,7 @@ describe("As an admin I want to manage categories", () => { name: secondCategoryName, }).then(() => { cy.visit(urlList.categories).searchInTable(startsWith); - ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable); - 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"); - }); - }); + cy.deleteTwoFirstRecordsFromGridListAndValidate("CategoryBulkDelete"); }); }, ); diff --git a/cypress/e2e/catalog/collections.js b/cypress/e2e/catalog/collections.js index de2bc3525..7b38571cd 100644 --- a/cypress/e2e/catalog/collections.js +++ b/cypress/e2e/catalog/collections.js @@ -3,8 +3,7 @@ import faker from "faker"; -import { collectionRow } from "../../elements/catalog/collection-selectors"; -import { BUTTON_SELECTORS } from "../../elements/shared/button-selectors"; +import { BUTTON_SELECTORS } from "../../elements"; import { collectionDetailsUrl, urlList } from "../../fixtures/urlList"; import { createChannel } from "../../support/api/requests/Channels"; import { @@ -87,7 +86,7 @@ describe("As an admin I want to manage collections.", () => { const collectionName = `${startsWith}${faker.datatype.number()}`; let collection; - cy.visit(urlList.collections).expectSkeletonIsVisible(); + cy.visit(urlList.collections); createCollection(collectionName, false, defaultChannel).then( collectionResp => { collection = collectionResp; @@ -111,7 +110,7 @@ describe("As an admin I want to manage collections.", () => { const collectionName = `${startsWith}${faker.datatype.number()}`; let collection; - cy.visit(urlList.collections).expectSkeletonIsVisible(); + cy.visit(urlList.collections); createCollection(collectionName, true, defaultChannel).then( collectionResp => { collection = collectionResp; @@ -140,7 +139,7 @@ describe("As an admin I want to manage collections.", () => { channel = channelResp; updateChannelInProduct(product.id, channel.id); - cy.visit(urlList.collections).expectSkeletonIsVisible(); + cy.visit(urlList.collections); createCollection(collectionName, false, channel).then( collectionResp => { collection = collectionResp; @@ -179,7 +178,7 @@ describe("As an admin I want to manage collections.", () => { }) .then(({ product: productResp }) => (createdProduct = productResp)); - cy.visit(urlList.collections).expectSkeletonIsVisible(); + cy.visit(urlList.collections); createCollection(collectionName, true, defaultChannel).then( collectionResp => { collection = collectionResp; @@ -362,27 +361,14 @@ describe("As an admin I want to manage collections.", () => { const secondCollectionName = `${deleteSeveral}${startsWith}${faker.datatype.number()}`; let firstCollection; let secondCollection; - + cy.addAliasToGraphRequest("CollectionBulkDelete"); createCollectionRequest(firstCollectionName).then(collectionResp => { firstCollection = collectionResp; }); createCollectionRequest(secondCollectionName).then(collectionResp => { secondCollection = collectionResp; - - cy.visit(urlList.collections) - .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"); + cy.visit(urlList.collections); + cy.deleteTwoFirstRecordsFromGridListAndValidate("CollectionBulkDelete"); getCollection({ collectionId: firstCollection.id }) .its("collection") .should("be.null"); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 280da1732..d4c4414ce 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -18,8 +18,13 @@ import "@percy/cypress"; 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 { ensureCanvasStatic } from "../support/customCommands/sharedElementsOperations/canvas"; import cypressGrep from "../support/cypress-grep/support"; commandTimings(); @@ -121,6 +126,39 @@ Cypress.Commands.add("clickGridHeader", col => { 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( "uncaught:exception", diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 6f66557e1..8a75fd96f 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -2377,6 +2377,9 @@ "context": "option title", "string": "Prioritize warehouses by sorting order" }, + "FTYkgw": { + "string": "Delete collections" + }, "FWbv/u": { "context": "page header", "string": "Create Discount" @@ -2397,10 +2400,6 @@ "context": "postal codes, header", "string": "Postal codes" }, - "FcVEpe": { - "context": "collection publication date", - "string": "Unpublished" - }, "FemBUF": { "context": "header", "string": "Translations to {language}" @@ -3390,10 +3389,6 @@ "context": "customers section name", "string": "Customers" }, - "Mee46w": { - "context": "collection publication date", - "string": "Becomes published on {date}" - }, "MewrtN": { "context": "section header", "string": "Fulfillment" @@ -5848,10 +5843,6 @@ "dnbJKr": { "string": "This transaction doesn't have any events" }, - "dpY94C": { - "context": "collection publication date", - "string": "Published on {date}" - }, "dsJ+Wv": { "context": "note on export gift cards", "string": "Note: Only active and not used gift cards will be expored" @@ -5910,6 +5901,9 @@ "context": "button", "string": "Done" }, + "eRqx44": { + "string": "Search collections..." + }, "eUjFjW": { "string": "Permission group created" }, @@ -7691,9 +7685,6 @@ "context": "hide error log label in notification", "string": "Hide log" }, - "s97tLq": { - "string": "Search Collections" - }, "s9sOcC": { "context": "button", "string": "OK" diff --git a/src/collections/components/CollectionList/CollectionList.tsx b/src/collections/components/CollectionList/CollectionList.tsx deleted file mode 100644 index 5b522527e..000000000 --- a/src/collections/components/CollectionList/CollectionList.tsx +++ /dev/null @@ -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, - ChannelProps { - collections: RelayToFlat; -} - -const numberOfColumns = 4; - -const CollectionList: React.FC = props => { - const { - collections, - disabled, - settings, - sort, - onUpdateListSettings, - onSort, - isChecked, - selected, - selectedChannelId, - toggle, - toggleAll, - toolbar, - filterDependency, - } = props; - - const classes = useStyles(props); - const intl = useIntl(); - - return ( - - - onSort(CollectionListUrlSortField.name)} - className={classes.colName} - > - - - onSort(CollectionListUrlSortField.productCount)} - className={classes.colProducts} - > - - - onSort(CollectionListUrlSortField.available)} - className={classes.colAvailability} - disabled={ - !canBeSorted( - CollectionListUrlSortField.available, - !!selectedChannelId, - ) - } - tooltip={intl.formatMessage(commonTooltipMessages.noFilterSelected, { - filterName: filterDependency.label, - })} - > - - - - - - - - - - {renderCollection( - collections, - collection => { - const isSelected = collection ? isChecked(collection.id) : false; - const channel = collection?.channelListings?.find( - listing => listing?.channel?.id === selectedChannelId, - ); - return ( - collection.id)} - > - - toggle(collection.id)} - /> - - - {maybe(() => collection.name, )} - - - {maybe( - () => collection.products.totalCount, - , - )} - - - {(!collection && ) || - (channel ? ( - - ) : ( - - ))} - - - ); - }, - () => ( - - - - - - ), - )} - - - ); -}; - -CollectionList.displayName = "CollectionList"; -export default CollectionList; diff --git a/src/collections/components/CollectionList/index.ts b/src/collections/components/CollectionList/index.ts deleted file mode 100644 index 4f5db2333..000000000 --- a/src/collections/components/CollectionList/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./CollectionList"; -export * from "./CollectionList"; diff --git a/src/collections/components/CollectionList/messages.ts b/src/collections/components/CollectionList/messages.ts deleted file mode 100644 index 21783b3d5..000000000 --- a/src/collections/components/CollectionList/messages.ts +++ /dev/null @@ -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", - }, -}); diff --git a/src/collections/components/CollectionListDatagrid/CollectionListDatagrid.tsx b/src/collections/components/CollectionListDatagrid/CollectionListDatagrid.tsx new file mode 100644 index 000000000..ba68dd64e --- /dev/null +++ b/src/collections/components/CollectionListDatagrid/CollectionListDatagrid.tsx @@ -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 { + 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 ( + + 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={() => ( + + )} + /> + + + + + + ); +}; diff --git a/src/collections/components/CollectionListDatagrid/datagrid.test.ts b/src/collections/components/CollectionListDatagrid/datagrid.test.ts new file mode 100644 index 000000000..98718217c --- /dev/null +++ b/src/collections/components/CollectionListDatagrid/datagrid.test.ts @@ -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}}", + }); + }); +}); diff --git a/src/collections/components/CollectionListDatagrid/datagrid.ts b/src/collections/components/CollectionListDatagrid/datagrid.ts new file mode 100644 index 000000000..92131e0af --- /dev/null +++ b/src/collections/components/CollectionListDatagrid/datagrid.ts @@ -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, +): 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"] + ]; +} diff --git a/src/collections/components/CollectionListDatagrid/index.ts b/src/collections/components/CollectionListDatagrid/index.ts new file mode 100644 index 000000000..6cf4bff79 --- /dev/null +++ b/src/collections/components/CollectionListDatagrid/index.ts @@ -0,0 +1 @@ +export * from "./CollectionListDatagrid"; diff --git a/src/collections/components/CollectionListDatagrid/messages.ts b/src/collections/components/CollectionListDatagrid/messages.ts new file mode 100644 index 000000000..984e4e0d4 --- /dev/null +++ b/src/collections/components/CollectionListDatagrid/messages.ts @@ -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", + }, +}); diff --git a/src/collections/components/CollectionListDeleteButton/CollectionListDeleteButton.tsx b/src/collections/components/CollectionListDeleteButton/CollectionListDeleteButton.tsx new file mode 100644 index 000000000..f820d4dfe --- /dev/null +++ b/src/collections/components/CollectionListDeleteButton/CollectionListDeleteButton.tsx @@ -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 ( + + + + + + + + + + + + + + + - + {selectedCollectionIds.length > 0 && ( + + + + )} + + } /> - { + navigate(collectionUrl(id)); + }} + hasRowHover={!isFilterPresetOpen} + rowAnchor={collectionUrl} {...listProps} /> diff --git a/src/collections/fixtures.ts b/src/collections/fixtures.ts index 451579959..a40fc73f8 100644 --- a/src/collections/fixtures.ts +++ b/src/collections/fixtures.ts @@ -1,13 +1,12 @@ // @ts-strict-ignore import { CollectionDetailsQuery, - CollectionListQuery, CollectionPublished, } from "@dashboard/graphql"; -import { RelayToFlat } from "@dashboard/types"; import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json"; import { CollectionListFilterOpts } from "./components/CollectionListPage"; +import { Collections } from "./types"; const content = richTextEditorFixtures.richTextEditor; @@ -28,7 +27,7 @@ export const collectionListFilterOpts: CollectionListFilterOpts = { }, }; -export const collections: RelayToFlat = [ +export const collections: Collections = [ { __typename: "Collection", channelListings: [ diff --git a/src/collections/types.ts b/src/collections/types.ts new file mode 100644 index 000000000..9aad8bd37 --- /dev/null +++ b/src/collections/types.ts @@ -0,0 +1,7 @@ +import { CollectionListQuery } from "@dashboard/graphql"; +import { RelayToFlat } from "@dashboard/types"; + +export type Collections = RelayToFlat< + NonNullable +>; +export type Collection = Collections[number]; diff --git a/src/collections/urls.ts b/src/collections/urls.ts index 9193622fd..56f0bd449 100644 --- a/src/collections/urls.ts +++ b/src/collections/urls.ts @@ -24,8 +24,8 @@ export type CollectionListUrlFilters = Filters; export type CollectionListUrlDialog = "remove" | TabActionDialog; export enum CollectionListUrlSortField { name = "name", - available = "available", - productCount = "products", + availability = "availability", + productCount = "productCount", } export type CollectionListUrlSort = Sort; export type CollectionListUrlQueryParams = ActiveTab & diff --git a/src/collections/views/CollectionList/CollectionList.tsx b/src/collections/views/CollectionList/CollectionList.tsx index 46f930e39..e8ed992b2 100644 --- a/src/collections/views/CollectionList/CollectionList.tsx +++ b/src/collections/views/CollectionList/CollectionList.tsx @@ -1,15 +1,14 @@ // @ts-strict-ignore import ActionDialog from "@dashboard/components/ActionDialog"; import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext"; +import { useColumnPickerSettings } from "@dashboard/components/Datagrid/ColumnPicker/useColumnPickerSettings"; import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog"; -import SaveFilterTabDialog, { - SaveFilterTabDialogFormData, -} from "@dashboard/components/SaveFilterTabDialog"; +import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog"; import { useCollectionBulkDeleteMutation, useCollectionListQuery, } from "@dashboard/graphql"; -import useBulkActions from "@dashboard/hooks/useBulkActions"; +import { useFilterPresets } from "@dashboard/hooks/useFilterPresets"; import useListSettings from "@dashboard/hooks/useListSettings"; import useNavigator from "@dashboard/hooks/useNavigator"; import useNotifier from "@dashboard/hooks/useNotifier"; @@ -18,6 +17,7 @@ import usePaginator, { createPaginationState, PaginatorContext, } from "@dashboard/hooks/usePaginator"; +import { useRowSelection } from "@dashboard/hooks/useRowSelection"; import { commonMessages } from "@dashboard/intl"; import { maybe } from "@dashboard/misc"; import { ListViews } from "@dashboard/types"; @@ -27,8 +27,8 @@ import createSortHandler from "@dashboard/utils/handlers/sortHandler"; import { mapEdgesToItems, mapNodeToChoice } from "@dashboard/utils/maps"; import { getSortParams } from "@dashboard/utils/sort"; import { DialogContentText } from "@material-ui/core"; -import { DeleteIcon, IconButton } from "@saleor/macaw-ui"; -import React, { useEffect } from "react"; +import isEqual from "lodash/isEqual"; +import React, { useCallback, useEffect } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import CollectionListPage from "../../components/CollectionListPage/CollectionListPage"; @@ -38,14 +38,10 @@ import { CollectionListUrlQueryParams, } from "../../urls"; import { - deleteFilterTab, - getActiveFilters, getFilterOpts, getFilterQueryParam, - getFiltersCurrentTab, - getFilterTabs, getFilterVariables, - saveFilterTab, + storageUtils, } from "./filters"; import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort"; @@ -57,22 +53,29 @@ export const CollectionList: React.FC = ({ params }) => { const navigate = useNavigator(); const intl = useIntl(); const notify = useNotifier(); - const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( - params.ids, - ); const { updateListSettings, settings } = useListSettings( ListViews.COLLECTION_LIST, ); + const { columnPickerSettings } = useColumnPickerSettings("COLLECTION_LIST"); usePaginationReset(collectionListUrl, params, settings.rowNumber); + const { channel } = useAppChannel(false); + + const { + clearRowSelection, + selectedRowIds, + setClearDatagridRowSelectionCallback, + setSelectedRowIds, + } = useRowSelection(params); const [changeFilters, resetFilters, handleSearchChange] = createFilterHandlers({ - cleanupFn: reset, + cleanupFn: clearRowSelection, createUrl: collectionListUrl, getFilterQueryParam, navigate, params, + keepActiveTab: true, }); const { availableChannels } = useAppChannel(false); @@ -83,6 +86,23 @@ export const CollectionList: React.FC = ({ params }) => { 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 queryVariables = React.useMemo( () => ({ @@ -98,6 +118,8 @@ export const CollectionList: React.FC = ({ params }) => { variables: queryVariables, }); + const collections = mapEdgesToItems(data?.collections); + const [collectionBulkDelete, collectionBulkDeleteOpts] = useCollectionBulkDeleteMutation({ onCompleted: data => { @@ -107,14 +129,13 @@ export const CollectionList: React.FC = ({ params }) => { text: intl.formatMessage(commonMessages.savedChanges), }); refetch(); - reset(); + clearRowSelection(); closeModal(); } }, }); const filterOpts = getFilterOpts(params, channelOpts); - const tabs = getFilterTabs(); useEffect(() => { if (!canBeSorted(params.sort, !!selectedChannel)) { @@ -127,34 +148,11 @@ export const CollectionList: React.FC = ({ params }) => { } }, [params]); - const currentTab = getFiltersCurrentTab(params, tabs); - const [openModal, closeModal] = createDialogActionHandlers< CollectionListUrlDialog, CollectionListUrlQueryParams >(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({ pageInfo: maybe(() => data.collections.pageInfo), paginationState, @@ -163,53 +161,75 @@ export const CollectionList: React.FC = ({ 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 ( openModal("delete-search")} + onTabChange={onPresetChange} + onTabDelete={(id: number) => { + setPresetIdToDelete(id); + openModal("delete-search"); + }} onTabSave={() => openModal("save-search")} - tabs={tabs.map(tab => tab.name)} + onTabUpdate={onPresetUpdate} + tabs={presets.map(tab => tab.name)} + loading={loading} disabled={loading} - collections={mapEdgesToItems(data?.collections)} + columnPickerSettings={columnPickerSettings} + collections={collections} settings={settings} onSort={handleSort} onUpdateListSettings={updateListSettings} sort={getSortParams(params)} - toolbar={ - - openModal("remove", { - ids: listElements, - }) - } - > - - - } - isChecked={isSelected} - selected={listElements.length} - toggle={toggle} - toggleAll={toggleAll} selectedChannelId={selectedChannel?.id} filterOpts={filterOpts} onFilterChange={changeFilters} + selectedCollectionIds={selectedRowIds} + onSelectCollectionIds={handleSetSelectedCollectionIds} + hasPresetsChanged={hasPresetsChange} + onCollectionsDelete={() => + openModal("remove", { + ids: selectedRowIds, + }) + } /> params.ids.length > 0)} + open={ + params.action === "remove" && maybe(() => selectedRowIds.length > 0) + } onClose={closeModal} confirmButtonState={collectionBulkDeleteOpts.status} onConfirm={() => collectionBulkDelete({ variables: { - ids: params.ids, + ids: selectedRowIds, }, }) } @@ -225,9 +245,9 @@ export const CollectionList: React.FC = ({ params }) => { 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?}}" values={{ - counter: maybe(() => params.ids.length), + counter: maybe(() => selectedRowIds.length), displayQuantity: ( - {maybe(() => params.ids.length)} + {maybe(() => selectedRowIds.length)} ), }} /> @@ -237,14 +257,14 @@ export const CollectionList: React.FC = ({ params }) => { open={params.action === "save-search"} confirmButtonState="default" onClose={closeModal} - onSubmit={handleTabSave} + onSubmit={onPresetSave} /> tabs[currentTab - 1].name, "...")} + onSubmit={onPresetDelete} + tabName={maybe(() => presets[presetIdToDelete - 1].name, "...")} /> ); diff --git a/src/collections/views/CollectionList/filters.ts b/src/collections/views/CollectionList/filters.ts index e03010bd5..9948ee673 100644 --- a/src/collections/views/CollectionList/filters.ts +++ b/src/collections/views/CollectionList/filters.ts @@ -73,8 +73,9 @@ export function getFilterQueryParam( } } -export const { deleteFilterTab, getFilterTabs, saveFilterTab } = - createFilterTabUtils(COLLECTION_FILTERS_KEY); +export const storageUtils = createFilterTabUtils( + COLLECTION_FILTERS_KEY, +); export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } = createFilterUtils( diff --git a/src/collections/views/CollectionList/sort.ts b/src/collections/views/CollectionList/sort.ts index 1c7a8969f..7cdd72228 100644 --- a/src/collections/views/CollectionList/sort.ts +++ b/src/collections/views/CollectionList/sort.ts @@ -13,7 +13,7 @@ export function canBeSorted( case CollectionListUrlSortField.name: case CollectionListUrlSortField.productCount: return true; - case CollectionListUrlSortField.available: + case CollectionListUrlSortField.availability: return isChannelSelected; default: return false; @@ -26,7 +26,7 @@ export function getSortQueryField( switch (sort) { case CollectionListUrlSortField.name: return CollectionSortField.NAME; - case CollectionListUrlSortField.available: + case CollectionListUrlSortField.availability: return CollectionSortField.AVAILABILITY; case CollectionListUrlSortField.productCount: return CollectionSortField.PRODUCT_COUNT; diff --git a/src/components/Datagrid/ColumnPicker/ColumnPicker.tsx b/src/components/Datagrid/ColumnPicker/ColumnPicker.tsx index c5b9ef624..85595ef8b 100644 --- a/src/components/Datagrid/ColumnPicker/ColumnPicker.tsx +++ b/src/components/Datagrid/ColumnPicker/ColumnPicker.tsx @@ -19,7 +19,7 @@ import { ColumnCategory } from "./useColumns"; export interface ColumnPickerProps { staticColumns: AvailableColumn[]; - dynamicColumns?: AvailableColumn[]; + dynamicColumns?: AvailableColumn[] | null | undefined; selectedColumns: string[]; columnCategories?: ColumnCategory[]; columnPickerSettings?: string[]; diff --git a/src/components/Datagrid/ColumnPicker/ColumnPickerDynamicColumns.tsx b/src/components/Datagrid/ColumnPicker/ColumnPickerDynamicColumns.tsx index 3595ed4ed..5415d35eb 100644 --- a/src/components/Datagrid/ColumnPicker/ColumnPickerDynamicColumns.tsx +++ b/src/components/Datagrid/ColumnPicker/ColumnPickerDynamicColumns.tsx @@ -6,7 +6,7 @@ import { AvailableColumn } from "../types"; import messages from "./messages"; export interface ColumnPickerDynamicColumnsProps { - dynamicColumns: AvailableColumn[] | undefined; + dynamicColumns?: AvailableColumn[] | null | undefined; setExpanded: (value: React.SetStateAction) => void; handleToggle: (id: string) => void; selectedColumns: string[]; diff --git a/src/components/Datagrid/ColumnPicker/useColumnPickerSettings.ts b/src/components/Datagrid/ColumnPicker/useColumnPickerSettings.ts index c38f3f3a7..7e923ebf1 100644 --- a/src/components/Datagrid/ColumnPicker/useColumnPickerSettings.ts +++ b/src/components/Datagrid/ColumnPicker/useColumnPickerSettings.ts @@ -7,7 +7,8 @@ export type DatagridViews = | "PRODUCT_DETAILS" | "ORDER_LIST" | "ORDER_DETAILS" - | "ORDER_DRAFT_DETAILS"; + | "ORDER_DRAFT_DETAILS" + | "COLLECTION_LIST"; type DynamicColumnSettings = { [view in DatagridViews]: string[]; @@ -19,6 +20,7 @@ export const defaultDynamicColumns: DynamicColumnSettings = { ORDER_LIST: [], ORDER_DETAILS: [], ORDER_DRAFT_DETAILS: [], + COLLECTION_LIST: [], }; export const useColumnPickerSettings = (view: DatagridViews) => { diff --git a/src/components/Datagrid/Datagrid.tsx b/src/components/Datagrid/Datagrid.tsx index 58c1c5021..f562df328 100644 --- a/src/components/Datagrid/Datagrid.tsx +++ b/src/components/Datagrid/Datagrid.tsx @@ -107,7 +107,7 @@ export interface DatagridProps { rowAnchor?: (item: Item) => string; rowHeight?: number | ((index: number) => number); 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 = ({ diff --git a/src/config.ts b/src/config.ts index 97073b6a3..29718cf7e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -75,6 +75,7 @@ export const defaultListSettings: AppListViewSettings = { }, [ListViews.COLLECTION_LIST]: { rowNumber: PAGINATE_BY, + columns: ["name", "productCount", "availability"], }, [ListViews.CUSTOMER_LIST]: { rowNumber: PAGINATE_BY, diff --git a/src/misc.ts b/src/misc.ts index 6c5ec4fe7..5cc4b7594 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -563,8 +563,8 @@ export const getByUnmatchingId = export const findById = (id: string, list?: T[]) => list?.find(getById(id)); -const COLOR_WARNING = "#FBE5AC"; -const COLOR_WARNING_DARK = "#3E2F0A"; +export const COLOR_WARNING = "#FBE5AC"; +export const COLOR_WARNING_DARK = "#3E2F0A"; type CustomWarningColor = typeof COLOR_WARNING | typeof COLOR_WARNING_DARK; export const getStatusColor = (