Drop dynamic column toggles in column picker (#3878)

* Drop dynamic column toggles

* Replace removal icon

* Add changeset

* Add docs

* Update docs

* Fix types on order draft datagrid

* Adjust collections column picker to new architecture

* Adjust categories column picker to new architecture

* Test column picker: adding, removing and searching for dynamic columns in picker (#3901)

* test - column picker. Adding, removing and searching for dynamic columns in picker.

* update tests tame with TC ids

* removed unused row checks on products list view

* Lint files

---------

Co-authored-by: wojteknowacki <124166231+wojteknowacki@users.noreply.github.com>
This commit is contained in:
Michał Droń 2023-07-12 13:59:17 +02:00 committed by GitHub
parent 2ab11bb407
commit 66976d547b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 478 additions and 339 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": minor
---
Drop dynamic column toggles in column picker

View file

@ -0,0 +1,137 @@
/// <reference types="cypress"/>
/// <reference types="../../../support"/>
import { PRODUCT_DETAILS, SHARED_ELEMENTS } from "../../../elements";
import { PRODUCTS_LIST } from "../../../elements/catalog/products/products-list";
import { LOCAL_STORAGE_FOR_COLUMN_PICKER } from "../../../fixtures";
import { urlList } from "../../../fixtures/urlList";
import { ensureCanvasStatic } from "../../../support/customCommands/sharedElementsOperations/canvas";
import { columnPickerPage } from "../../../support/pages";
describe("As an admin I should be able to use column picker", () => {
beforeEach(() => {
cy.clearSessionData().loginUserViaRequest();
});
it(
"should be able to add new dynamic column to grid on product list via search. TC: SALEOR_2610",
{ tags: ["@critical", "@allEnv", "@stable"] },
() => {
const dynamicColumnToBeSearched = "ABV";
cy.addAliasToGraphRequest("AvailableColumnAttributes");
cy.visit(urlList.products);
ensureCanvasStatic(PRODUCTS_LIST.dataGridTable);
columnPickerPage.openColumnPicker();
columnPickerPage.openDynamicColumnsSearch();
columnPickerPage.typeNameInSearchColumnInput(dynamicColumnToBeSearched);
cy.waitForRequestAndCheckIfNoErrors("@AvailableColumnAttributes");
cy.get(SHARED_ELEMENTS.dynamicColumnSelector)
.should("have.length", 1)
.should("contain.text", dynamicColumnToBeSearched)
.find("button")
.click();
cy.get(SHARED_ELEMENTS.dynamicColumnSelector)
.invoke("text")
.then(selectedColumnName => {
cy.get(SHARED_ELEMENTS.dynamicColumnContainer)
// do not check by visible text just data-test-id since often text has ellipsis
.find(`[data-test-id="column-name-${selectedColumnName}"]`)
.should("be.visible");
// newly added dynamic column is alway placed as last one on grid
cy.get(SHARED_ELEMENTS.dataGridTable)
.find("th")
.last()
.should("have.text", selectedColumnName);
});
},
);
it(
"should be able to remove dynamic column from picker on products list. TC: SALEOR_2611",
{ tags: ["@productsList", "@allEnv", "@stable"] },
() => {
const listConfigLocalStorage = JSON.stringify(
LOCAL_STORAGE_FOR_COLUMN_PICKER.listConfigWithAttributeColumnPicker,
);
// local storage is updated to avoid not necessary action of adding dynamic column in the beginning of test
cy.window().then(win => {
win.localStorage.setItem("listConfig", listConfigLocalStorage);
});
cy.visit(urlList.products);
ensureCanvasStatic(PRODUCTS_LIST.dataGridTable);
columnPickerPage.openColumnPicker();
cy.get(SHARED_ELEMENTS.dynamicColumnContainer)
.find(SHARED_ELEMENTS.selectedDynamicColumnNameSelector)
.should("have.length", 1)
.invoke("text")
.then(columnName => {
cy.get(SHARED_ELEMENTS.dynamicColumnContainer)
.find(SHARED_ELEMENTS.removeSelectedDynamicColumnButton)
.should("be.visible")
.should("have.length", 1)
.click();
cy.get(SHARED_ELEMENTS.dynamicColumnContainer)
.find(SHARED_ELEMENTS.removeSelectedDynamicColumnButton)
.should("not.exist");
cy.get(SHARED_ELEMENTS.dynamicColumnContainer)
.find(columnName)
.should("not.exist");
});
},
);
it(
"should validate: that there is always at least one active static column, use pagination when searching dynamic columns, hiding column picker works. TC: SALEOR_2612",
{ tags: ["@productsList", "@allEnv", "@stable"] },
() => {
cy.addAliasToGraphRequest("ProductDetails");
// local storage accepts only strings
const listConfigLocalStorage = JSON.stringify(
LOCAL_STORAGE_FOR_COLUMN_PICKER.localStorageWithSingleStaticColumn,
);
// local storage is updated to make sure only one static column is active
cy.window().then(win => {
win.localStorage.setItem("listConfig", listConfigLocalStorage);
});
cy.visit(urlList.products);
ensureCanvasStatic(PRODUCTS_LIST.dataGridTable);
columnPickerPage.openColumnPicker();
cy.get(SHARED_ELEMENTS.activeStaticColumnOnGridButton).should(
"have.length",
1,
"There should be only one active static column",
);
columnPickerPage.openDynamicColumnsSearch();
cy.get(SHARED_ELEMENTS.paginationBackOnColumnPicker).should(
"have.attr",
"disabled",
);
cy.get(SHARED_ELEMENTS.paginationForwardOnColumnPicker).click();
cy.get(SHARED_ELEMENTS.paginationBackOnColumnPicker).should(
"not.have.attr",
"disabled",
);
columnPickerPage
.selectDynamicColumnAtIndex(1)
.invoke("text")
.then(selectedColumnName => {
cy.get(SHARED_ELEMENTS.dynamicColumnContainer)
// do not check by visible text just data-test-id since often text has ellipsis
.find(`[data-test-id*="${selectedColumnName}"]`)
.should("be.visible");
// newly added dynamic column is alway placed as last one on grid
cy.get(SHARED_ELEMENTS.dataGridTable)
.find("th")
.last()
.should("have.text", selectedColumnName);
//next line hides picker
cy.get(SHARED_ELEMENTS.pageHeader).click({ force: true });
cy.get(SHARED_ELEMENTS.dynamicColumnContainer).should("not.exist");
// now it checks does picking record from grid works when picker is gone
cy.clickGridCell(1, 1);
cy.waitForRequestAndCheckIfNoErrors("@ProductDetails");
cy.get(PRODUCT_DETAILS.productUpdateFormSection).should("be.visible");
});
},
);
});

View file

@ -28,4 +28,5 @@ export const PRODUCT_DETAILS = {
editVariant: '[data-test-id="row-action-button"]',
firstRowDataGrid: "[data-testid='glide-cell-1-0']",
dataGridTable: "[data-testid='data-grid-canvas']",
productUpdateFormSection: "[data-test-id='product-update-form']",
};

View file

@ -9,7 +9,19 @@ export const SHARED_ELEMENTS = {
table: 'table[class*="Table"]',
firstRowDataGrid: "[data-testid='glide-cell-1-0']",
secondRowDataGrid: "[id='glide-cell-1-1']",
openColumnPickerButton: "[data-test-id='open-column-picker-button']",
openDynamicColumnsSearchButton: "[data-test-id='open-dynamic-search']",
tableRow: '[data-test-id*="id"], [class*="MuiTableRow"]',
activeStaticColumnOnGridButton: '[data-state="on"]',
dynamicColumnSelector: '[data-test-id="dynamic-column"]',
dynamicColumnNameSelector: '[data-test-id^="dynamic-column-name"]',
dynamicColumnSearchInput: '[data-test-id="search-columns"]',
selectedDynamicColumnNameSelector: '[data-test-id^="column-name-"]',
removeSelectedDynamicColumnButton:
'[data-test-id^="remove-dynamic-col-button"]',
dynamicColumnContainer: '[data-test-id="dynamic-col-container"]',
paginationForwardOnColumnPicker: '[data-test-id="pagination-forward"]',
paginationBackOnColumnPicker: '[data-test-id="pagination-back"]',
notificationSuccess:
'[data-test-id="notification"][data-test-type="success"]',
notificationFailure: '[data-test-id="notification"][data-test-type="error"]',

View file

@ -3,3 +3,4 @@ export { orderDraftCreateDemoResponse } from "./errors/demo/orderDratCreate";
export { urlList } from "./urlList";
export { ONE_PERMISSION_USERS, TEST_ADMIN_USER } from "./users";
export { MESSAGES } from "./messages";
export * as LOCAL_STORAGE_FOR_COLUMN_PICKER from "./localStorage/columnPickerMocks";

View file

@ -0,0 +1,139 @@
export const listConfigWithAttributeColumnPicker = {
APPS_LIST: {
rowNumber: 100,
},
ATTRIBUTE_VALUE_LIST: {
rowNumber: 10,
},
CATEGORY_LIST: {
rowNumber: 20,
},
COLLECTION_LIST: {
rowNumber: 20,
},
CUSTOMER_LIST: {
rowNumber: 20,
},
DRAFT_LIST: {
rowNumber: 20,
columns: ["number", "date", "customer", "total"],
},
NAVIGATION_LIST: {
rowNumber: 20,
},
ORDER_LIST: {
rowNumber: 20,
columns: ["number", "date", "customer", "payment", "status", "total"],
},
PAGES_LIST: {
rowNumber: 20,
},
PLUGIN_LIST: {
rowNumber: 20,
},
PRODUCT_LIST: {
columns: [
"name",
"availability",
"description",
"price",
"productType",
"date",
"attribute:QXR0cmlidXRlOjIx",
],
rowNumber: 20,
},
SALES_LIST: {
rowNumber: 20,
},
SHIPPING_METHODS_LIST: {
rowNumber: 20,
},
STAFF_MEMBERS_LIST: {
rowNumber: 20,
},
PERMISSION_GROUP_LIST: {
rowNumber: 20,
},
VOUCHER_LIST: {
rowNumber: 20,
},
WAREHOUSE_LIST: {
rowNumber: 20,
},
WEBHOOK_LIST: {
rowNumber: 20,
},
TRANSLATION_ATTRIBUTE_VALUE_LIST: {
rowNumber: 10,
},
" GIFT_CARD_LIST": {
rowNumber: 20,
},
};
export const localStorageWithSingleStaticColumn = {
APPS_LIST: {
rowNumber: 100,
},
ATTRIBUTE_VALUE_LIST: {
rowNumber: 10,
},
CATEGORY_LIST: {
rowNumber: 20,
},
COLLECTION_LIST: {
rowNumber: 20,
},
CUSTOMER_LIST: {
rowNumber: 20,
},
DRAFT_LIST: {
rowNumber: 20,
columns: ["number", "date", "customer", "total"],
},
NAVIGATION_LIST: {
rowNumber: 20,
},
ORDER_LIST: {
rowNumber: 20,
columns: ["number", "date", "customer", "payment", "status", "total"],
},
PAGES_LIST: {
rowNumber: 20,
},
PLUGIN_LIST: {
rowNumber: 20,
},
PRODUCT_LIST: {
columns: ["name"],
rowNumber: 20,
},
SALES_LIST: {
rowNumber: 20,
},
SHIPPING_METHODS_LIST: {
rowNumber: 20,
},
STAFF_MEMBERS_LIST: {
rowNumber: 20,
},
PERMISSION_GROUP_LIST: {
rowNumber: 20,
},
VOUCHER_LIST: {
rowNumber: 20,
},
WAREHOUSE_LIST: {
rowNumber: 20,
},
WEBHOOK_LIST: {
rowNumber: 20,
},
TRANSLATION_ATTRIBUTE_VALUE_LIST: {
rowNumber: 10,
},
" GIFT_CARD_LIST": {
rowNumber: 20,
},
};

View file

@ -0,0 +1,22 @@
import { SHARED_ELEMENTS } from "../../elements/shared/sharedElements";
export function openColumnPicker() {
cy.get(SHARED_ELEMENTS.openColumnPickerButton).click();
}
export function openDynamicColumnsSearch() {
cy.get(SHARED_ELEMENTS.openDynamicColumnsSearchButton).click();
}
export function selectDynamicColumnAtIndex(columnIndex) {
return cy
.get(SHARED_ELEMENTS.dynamicColumnNameSelector)
.eq(columnIndex)
.click();
}
export function typeNameInSearchColumnInput(columnName) {
return cy
.get(SHARED_ELEMENTS.dynamicColumnSearchInput)
.should("be.visible")
.click()
.type(columnName)
.blur();
}

View file

@ -38,4 +38,5 @@ export * as priceListComponent from "./catalog/products/priceListComponent";
export * as variantsPage from "./catalog/products/VariantsPage";
export * as channelsPage from "./channelsPage";
export * as pagesPage from "./pagesPage";
export * as columnPickerPage from "./columnPicker";
export * as pageDetailsPage from "./pageDetailsPage";

View file

@ -110,7 +110,7 @@ export const CategoryListDatagrid = ({
onRowSelectionChange={onSelectCategoriesIds}
renderColumnPicker={() => (
<ColumnPicker
onSave={handlers.onChange}
onToggle={handlers.onToggle}
selectedColumns={selectedColumns}
staticColumns={staticColumns}
/>

View file

@ -27,7 +27,6 @@ interface CollectionListDatagridProps
SortPage<CollectionListUrlSortField> {
collections: Collections;
loading: boolean;
columnPickerSettings: string[];
selectedChannelId: string;
hasRowHover?: boolean;
onSelectCollectionIds: (
@ -48,7 +47,6 @@ export const CollectionListDatagrid = ({
onRowClick,
rowAnchor,
disabled,
columnPickerSettings,
onSelectCollectionIds,
onSort,
filterDependency,
@ -76,9 +74,7 @@ export const CollectionListDatagrid = ({
handlers,
visibleColumns,
staticColumns,
dynamicColumns,
selectedColumns,
columnCategories,
recentlyAddedColumn,
} = useColumns({
staticColumns: collectionListStaticColumns,
@ -181,12 +177,8 @@ export const CollectionListDatagrid = ({
renderColumnPicker={() => (
<ColumnPicker
staticColumns={staticColumns}
dynamicColumns={dynamicColumns}
selectedColumns={selectedColumns}
columnCategories={columnCategories}
onDynamicColumnSelect={handlers.onDynamicColumnSelect}
columnPickerSettings={columnPickerSettings}
onSave={handlers.onChange}
onToggle={handlers.onToggle}
/>
)}
/>

View file

@ -34,7 +34,6 @@ const props: CollectionListPageProps = {
collections,
selectedChannelId: "123",
filterOpts: collectionListFilterOpts,
columnPickerSettings: ["name"],
selectedCollectionIds: [],
hasPresetsChanged: () => false,
onAll: () => undefined,

View file

@ -34,7 +34,6 @@ export interface CollectionListPageProps
SortPage<CollectionListUrlSortField> {
onTabUpdate: (tabName: string) => void;
selectedChannelId: string;
columnPickerSettings: string[];
collections: Collections;
loading: boolean;
selectedCollectionIds: string[];

View file

@ -1,7 +1,6 @@
// @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 from "@dashboard/components/SaveFilterTabDialog";
import {
@ -56,7 +55,6 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
const { updateListSettings, settings } = useListSettings(
ListViews.COLLECTION_LIST,
);
const { columnPickerSettings } = useColumnPickerSettings("COLLECTION_LIST");
usePaginationReset(collectionListUrl, params, settings.rowNumber);
const { channel } = useAppChannel(false);
@ -211,7 +209,6 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
tabs={presets.map(tab => tab.name)}
loading={loading}
disabled={loading}
columnPickerSettings={columnPickerSettings}
collections={collections}
settings={settings}
onSort={handleSort}

View file

@ -2,7 +2,7 @@
### System Architecture
<img width="977" alt="image" src="https://user-images.githubusercontent.com/41952692/233042483-d2cb30f3-26b7-40b5-9d08-2ea42f7f0242.png">
<img width="858" alt="image" src="https://github.com/saleor/saleor-dashboard/assets/41952692/8ce49003-e2af-4901-9098-fa5deb9bbe66">
### Column types
@ -34,35 +34,32 @@ attribute:QXR0cmlidXRlOjIx
- dynamic columns - array of dynamic columns for the column picker
- column categories - array of column categories, which is abstraction for dynamic column. For example attributes is a column category, whereas Flavor attribute is an actual column value. This object has all API-related properties, like search handler, fetch more props, etc.
- selected columns - array of column IDs which are selected in the column picker. It is saved in local storage
- dynamic column settings - array of column IDs which are selected in the left section of the column picker. It is saved in local storage.
- recently added column - this value is used in datagrid component to enable auto-scroll to newly added column
- handlers:
- column resize handler (for datagrid)
- column reorder handler (for datagrid)
- column visibility handler (for column picker)
- dynamic column selection handler (for column picker)
- column selection toggle (for column picker)
- customUpdateVisible - used to manually update visible columns state. For now it is required to update arrow icon in the datagrid columns
In order to use this hook, you need to provide four things:
In order to use this hook, you need to provide two/three things:
- `staticColumns` - array of static columns in datagrid-ready format (`AvailableColumns[]`)
- `columnCategories` - array of column categories
- `columnCategories` - array of column categories (only if there are any dynamic columns)
- state & setter of column settings which we get from `useListSettings`
- state of column picker settings which we get from `useColumnPickerSettings`
## Adapting new views
### Column picker settings
### Selected columns in LS
Firstly, in the view file, we need to provide two settings object, one for the selected columns and one for the dynamic column settings. We should use `useColumnPickerSettings` and `useListSettings` hook for that. The first settings object manages columns selected for the datagrid (visible columns). The second manages state of seleceted dynamic columns (if we pick a value from left side of column picked, it is then displayed on the right side of the picker as dynamic column with togglable visibility). Toggling the visiblity saves the column in the first settings object.
The reason why column picker settings object needs to be in the view file and cannot be integrated into internal logic of useColumns is because we use column picker settings in the query. We need to know which columns are selected in order to fetch the correct data from the API.
Firstly, in the view file, we need to provide settings object which holds seleted columns IDs. We should use `useListSettings` hook for that.
```tsx
const { columnPickerSettings, setDynamicColumnsSettings } =
useColumnPickerSettings("PRODUCT_LIST");
const { updateListSettings, settings } = useListSettings(
ListViews.PRODUCT_LIST,
);
// Translates columnIDs to api IDs
const filteredColumnIds = columnPickerSettings
const filteredColumnIds = settings.columns
.filter(isAttributeColumnValue)
.map(getAttributeIdFromColumnValue);
@ -131,12 +128,12 @@ export const parseDynamicColumnsForProductListView = ({
name: "Attributes",
prefix: "attribute",
availableNodes: parseAttributesColumns(
attributesData,
attributesData, // all attributes
activeAttributeSortId,
sort,
),
selectedNodes: parseAttributesColumns(
gridAttributesData,
gridAttributesData, // selected attributes
activeAttributeSortId,
sort,
),
@ -151,7 +148,7 @@ export const parseDynamicColumnsForProductListView = ({
Here we only have 1 column category, attributes. `attributesData` is the result of the first query, `gridAttributesData` is the result of the second query. We also provide pagination props, which are used in the column picker.
Queries which are used in this case are for categories. Let's look at the first query:
Let's have a look at the first query:
```tsx
export const availableColumnAttribues = gql`
@ -185,7 +182,7 @@ export const availableColumnAttribues = gql`
This query is used to fetch all **available** attributes. It is paginated and has a search filter and results are displayed in the left part of the column picker.
The second query is similar, but it has a filter of IDs, which come from local storage settings (useColumnPickerSettngs):
The second query is similar, but it has a filter of IDs, which come from local storage settings (useListSettings):
```tsx
export const gridAttributes = gql`

View file

@ -22,9 +22,7 @@ export interface ColumnPickerProps {
dynamicColumns?: AvailableColumn[] | null | undefined;
selectedColumns: string[];
columnCategories?: ColumnCategory[];
columnPickerSettings?: string[];
onSave: (columns: string[]) => void;
onDynamicColumnSelect?: (columns: string[]) => void;
onToggle: (columnId: string) => void;
}
export const ColumnPicker = ({
@ -32,23 +30,11 @@ export const ColumnPicker = ({
selectedColumns,
columnCategories,
dynamicColumns,
columnPickerSettings,
onDynamicColumnSelect,
onSave,
onToggle,
}: ColumnPickerProps) => {
const [pickerOpen, setPickerOpen] = useState(false);
const [expanded, setExpanded] = useState(false);
const renderCategories =
columnCategories &&
typeof onDynamicColumnSelect === "function" &&
columnPickerSettings;
const handleToggle = (id: string) =>
selectedColumns.includes(id)
? onSave(selectedColumns.filter(currentId => currentId !== id))
: onSave([...selectedColumns, id]);
return (
<Popover
modal
@ -60,6 +46,7 @@ export const ColumnPicker = ({
>
<Popover.Trigger>
<Button
data-test-id="open-column-picker-button"
variant="tertiary"
icon={<TableEditIcon />}
pointerEvents={pickerOpen ? "none" : undefined}
@ -79,11 +66,11 @@ export const ColumnPicker = ({
gridTemplateColumns={expanded ? 2 : 1}
overflow="hidden"
>
{expanded && renderCategories && (
{expanded && columnCategories && (
<ColumnPickerCategories
columnCategories={columnCategories}
columnPickerSettings={columnPickerSettings}
onDynamicColumnSelect={onDynamicColumnSelect}
selectedColumns={selectedColumns}
onToggle={onToggle}
onClose={() => setExpanded(false)}
/>
)}
@ -102,15 +89,14 @@ export const ColumnPicker = ({
</Box>
<ColumnPickerStaticColumns
staticColumns={staticColumns}
handleToggle={handleToggle}
handleToggle={onToggle}
selectedColumns={selectedColumns}
/>
{columnCategories && (
<ColumnPickerDynamicColumns
dynamicColumns={dynamicColumns}
selectedColumns={selectedColumns}
setExpanded={setExpanded}
handleToggle={handleToggle}
onToggle={onToggle}
/>
)}
</Box>

View file

@ -9,18 +9,18 @@ import { ColumnCategory } from "./useColumns";
export interface ColumnPickerAvailableNodesProps {
currentCategory: ColumnCategory;
columnPickerSettings: string[];
selectedColumns: string[];
query: string;
setQuery: React.Dispatch<React.SetStateAction<string>>;
changeHandler: (column: string) => void;
onToggle: (column: string) => void;
}
export const ColumnPickerAvailableNodes = ({
currentCategory,
columnPickerSettings,
selectedColumns,
query,
setQuery,
changeHandler,
onToggle,
}: ColumnPickerAvailableNodesProps) => {
const areNodesLoading = currentCategory.availableNodes === undefined;
const areNodesEmpty = currentCategory.availableNodes?.length === 0;
@ -51,11 +51,16 @@ export const ColumnPickerAvailableNodes = ({
return currentCategory.availableNodes!.map(node => (
<Box padding={2} key={node.id}>
<Checkbox
onCheckedChange={() => changeHandler(node.id)}
checked={columnPickerSettings.includes(node.id)}
data-test-id={`search-dynamic-${node.id}`}
onCheckedChange={() => onToggle(node.id)}
checked={selectedColumns.includes(node.id)}
data-test-id={`dynamic-column`}
>
<Text size="small" color="textNeutralSubdued" ellipsis>
<Text
data-test-id={`dynamic-column-name-${node.title}`}
size="small"
color="textNeutralSubdued"
ellipsis
>
{node.title}
</Text>
</Checkbox>

View file

@ -13,28 +13,21 @@ import { getExitIcon, getExitOnClick } from "./utils";
export interface ColumnPickerCategoriesProps {
columnCategories: ColumnCategory[];
columnPickerSettings: string[];
selectedColumns: string[];
onClose: () => void;
onDynamicColumnSelect: (columns: string[]) => void;
onToggle: (columnId: string) => void;
}
export const ColumnPickerCategories = ({
columnCategories,
onClose,
onDynamicColumnSelect,
columnPickerSettings,
onToggle,
selectedColumns,
}: ColumnPickerCategoriesProps) => {
const { currentCategory, setCurrentCategory } =
useCategorySelection(columnCategories);
const { query, setQuery } = useAvailableColumnsQuery(currentCategory);
const changeHandler = (column: string) =>
columnPickerSettings.includes(column)
? onDynamicColumnSelect(
columnPickerSettings.filter(currentCol => currentCol !== column),
)
: onDynamicColumnSelect([...columnPickerSettings, column]);
return (
<Box
backgroundColor="subdued"
@ -73,10 +66,10 @@ export const ColumnPickerCategories = ({
{currentCategory ? (
<ColumnPickerAvailableNodes
currentCategory={currentCategory}
columnPickerSettings={columnPickerSettings}
selectedColumns={selectedColumns}
query={query}
setQuery={setQuery}
changeHandler={changeHandler}
onToggle={onToggle}
/>
) : (
<ColumnPickerCategoryList

View file

@ -1,4 +1,4 @@
import { Box, Button, PlusIcon, Text, Toggle } from "@saleor/macaw-ui/next";
import { Box, Button, PlusIcon, RemoveIcon, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { FormattedMessage } from "react-intl";
@ -8,15 +8,13 @@ import messages from "./messages";
export interface ColumnPickerDynamicColumnsProps {
dynamicColumns?: AvailableColumn[] | null | undefined;
setExpanded: (value: React.SetStateAction<boolean>) => void;
handleToggle: (id: string) => void;
selectedColumns: string[];
onToggle: (id: string) => void;
}
export const ColumnPickerDynamicColumns = ({
dynamicColumns,
setExpanded,
handleToggle,
selectedColumns,
onToggle,
}: ColumnPickerDynamicColumnsProps) => (
<Box data-test-id="dynamic-col-container">
<Box
@ -37,24 +35,39 @@ export const ColumnPickerDynamicColumns = ({
/>
</Box>
{dynamicColumns?.map(column => (
<Box padding={1} key={column.id}>
<Toggle
onPressedChange={() => handleToggle(column.id)}
pressed={selectedColumns.includes(column.id)}
data-test-id={`dynamic-col-${column.id}`}
<Box
display="flex"
alignItems="center"
gap={2}
padding={1}
key={column.id}
>
<Button
onClick={() => onToggle(column.id)}
data-test-id={`remove-dynamic-col-button-${column.title}`}
variant="tertiary"
size="small"
icon={<RemoveIcon color="iconNeutralPlain" />}
__width="20px"
__height="20px"
/>
<Text
variant="body"
size="small"
color="textNeutralSubdued"
whiteSpace="nowrap"
>
<Text
variant="body"
size="small"
color="textNeutralSubdued"
whiteSpace="nowrap"
>
{`${column.metaGroup} /`}
</Text>
<Text variant="body" size="small" color="textNeutralDefault" ellipsis>
{column.title}
</Text>
</Toggle>
{`${column.metaGroup} /`}
</Text>
<Text
variant="body"
size="small"
color="textNeutralDefault"
ellipsis
data-test-id={`column-name-${column.title}`}
>
{column.title}
</Text>
</Box>
))}
</Box>

View file

@ -1,41 +0,0 @@
import useLocalStorage from "@dashboard/hooks/useLocalStorage";
const COLUMN_PICKER_KEY = "columnPickerConfig";
export type DatagridViews =
| "PRODUCT_LIST"
| "PRODUCT_DETAILS"
| "ORDER_LIST"
| "ORDER_DETAILS"
| "ORDER_DRAFT_DETAILS"
| "COLLECTION_LIST";
type DynamicColumnSettings = {
[view in DatagridViews]: string[];
};
export const defaultDynamicColumns: DynamicColumnSettings = {
PRODUCT_LIST: [],
PRODUCT_DETAILS: [],
ORDER_LIST: [],
ORDER_DETAILS: [],
ORDER_DRAFT_DETAILS: [],
COLLECTION_LIST: [],
};
export const useColumnPickerSettings = (view: DatagridViews) => {
const [config, setConfig] = useLocalStorage(
COLUMN_PICKER_KEY,
defaultDynamicColumns,
);
const setDynamicColumnsSettings = (cols: string[]) =>
setConfig(currentSettings => ({
...currentSettings,
[view]: cols,
}));
const columnPickerSettings = config[view] ?? [];
return { columnPickerSettings, setDynamicColumnsSettings };
};

View file

@ -8,21 +8,22 @@ const mockedColumns: AvailableColumn[] = [
id: "name",
title: "Name",
width: 200,
metaGroup: "Product",
hasMenu: false,
icon: "arrowUp",
},
{
id: "description",
title: "Description",
width: 100,
metaGroup: "Sales Information",
hasMenu: false,
icon: "arrowUp",
},
];
const mockedSelectedColumns = ["name", "attribute:QXR0cmlidXRlOjE0"];
// dynamic - color and ABV
const mockedSelectedColumns = [
"name",
"attribute:QXR0cmlidXRlOjE0",
"attribute:QXR0cmlidXRlOjIx",
];
const mockedCategories: ColumnCategory[] = [
{
@ -67,30 +68,12 @@ const mockedCategories: ColumnCategory[] = [
metaGroup: "Attributes",
width: 200,
},
{
id: "attribute:QXR0cmlidXRlOjE1",
title: "Bottle Size",
metaGroup: "Attributes",
width: 200,
},
{
id: "attribute:QXR0cmlidXRlOjE0",
title: "Color",
metaGroup: "Attributes",
width: 200,
},
{
id: "attribute:QXR0cmlidXRlOjUwOQ==",
title: "storage size ",
metaGroup: "Attributes",
width: 200,
},
{
id: "attribute:QXR0cmlidXRlOjI5",
title: "Tag",
metaGroup: "Attributes",
width: 200,
},
],
hasNextPage: false,
hasPreviousPage: false,
@ -100,21 +83,11 @@ const mockedCategories: ColumnCategory[] = [
},
];
const mockedColumnPickerSettings = [
"attribute:QXR0cmlidXRlOjIx",
"attribute:QXR0cmlidXRlOjE1",
"attribute:QXR0cmlidXRlOjUwOQ==",
"attribute:QXR0cmlidXRlOjI5",
"attribute:QXR0cmlidXRlOjE0",
];
const expectedVisibleColumns = [
{
id: "name",
title: "Name",
width: 200,
metaGroup: "Product",
hasMenu: false,
icon: "arrowUp",
},
{
@ -123,43 +96,29 @@ const expectedVisibleColumns = [
metaGroup: "Attributes",
width: 200,
},
];
// In order of mockedColumnPickerSettings
const expectedDynamicColumns = [
{
id: "attribute:QXR0cmlidXRlOjIx",
title: "ABV",
metaGroup: "Attributes",
width: 200,
},
{
id: "attribute:QXR0cmlidXRlOjE1",
title: "Bottle Size",
metaGroup: "Attributes",
width: 200,
},
{
id: "attribute:QXR0cmlidXRlOjUwOQ==",
title: "storage size ",
metaGroup: "Attributes",
width: 200,
},
{
id: "attribute:QXR0cmlidXRlOjI5",
title: "Tag",
metaGroup: "Attributes",
width: 200,
},
];
const expectedDynamicColumns = [
{
id: "attribute:QXR0cmlidXRlOjE0",
title: "Color",
metaGroup: "Attributes",
width: 200,
},
{
id: "attribute:QXR0cmlidXRlOjIx",
title: "ABV",
metaGroup: "Attributes",
width: 200,
},
];
const setDynamicColumnSettings = jest.fn();
const onSave = jest.fn();
describe("useColumns", () => {
@ -174,8 +133,6 @@ describe("useColumns", () => {
selectedColumns: mockedSelectedColumns,
columnCategories: mockedCategories,
onSave,
columnPickerSettings: mockedColumnPickerSettings,
setDynamicColumnSettings,
}),
);
// Assert
@ -184,9 +141,6 @@ describe("useColumns", () => {
expect(result.current.dynamicColumns).toEqual(expectedDynamicColumns);
expect(result.current.selectedColumns).toEqual(mockedSelectedColumns);
expect(result.current.columnCategories).toEqual(mockedCategories);
expect(result.current.columnPickerSettings).toEqual(
mockedColumnPickerSettings,
);
});
it("should update visible column info when resized", () => {
// Arrange
@ -196,8 +150,6 @@ describe("useColumns", () => {
selectedColumns: mockedSelectedColumns,
columnCategories: mockedCategories,
onSave,
columnPickerSettings: mockedColumnPickerSettings,
setDynamicColumnSettings,
}),
);
@ -217,8 +169,6 @@ describe("useColumns", () => {
selectedColumns: mockedSelectedColumns,
columnCategories: mockedCategories,
onSave,
columnPickerSettings: mockedColumnPickerSettings,
setDynamicColumnSettings,
}),
);
@ -226,11 +176,28 @@ describe("useColumns", () => {
act(() => result.current.handlers.onMove(1, 0));
// Assert
expect(result.current.visibleColumns).toEqual(
expectedVisibleColumns.reverse(),
);
expect(result.current.visibleColumns).toEqual([
{
id: "attribute:QXR0cmlidXRlOjE0",
title: "Color",
metaGroup: "Attributes",
width: 200,
},
{
id: "name",
title: "Name",
width: 200,
icon: "arrowUp",
},
{
id: "attribute:QXR0cmlidXRlOjIx",
title: "ABV",
metaGroup: "Attributes",
width: 200,
},
]);
});
it("should call onSave when column is changed", () => {
it("should call onSave when column is toggled", () => {
// Arrange
const { result } = renderHook(() =>
useColumns({
@ -238,13 +205,11 @@ describe("useColumns", () => {
selectedColumns: mockedSelectedColumns,
columnCategories: mockedCategories,
onSave,
columnPickerSettings: mockedColumnPickerSettings,
setDynamicColumnSettings,
}),
);
// Act
act(() => result.current.handlers.onChange(["name"]));
act(() => result.current.handlers.onToggle("name"));
// Assert
expect(onSave).toHaveBeenCalledTimes(1);
@ -257,23 +222,15 @@ describe("useColumns", () => {
selectedColumns: mockedSelectedColumns,
columnCategories: mockedCategories,
onSave,
columnPickerSettings: mockedColumnPickerSettings,
setDynamicColumnSettings,
}),
);
// Act
act(() =>
result.current.handlers.onChange([
"name",
"attribute:QXR0cmlidXRlOjE0",
"attribute:QXR0cmlidXRlOjIx",
]),
);
act(() => result.current.handlers.onToggle("attribute:QXR0cmlidXRlOjI3"));
// Assert
expect(result.current.recentlyAddedColumn).toEqual(
"attribute:QXR0cmlidXRlOjIx",
"attribute:QXR0cmlidXRlOjI3",
);
});
it("should update dynamic columns when new column is picked", () => {
@ -284,21 +241,14 @@ describe("useColumns", () => {
selectedColumns: mockedSelectedColumns,
columnCategories: mockedCategories,
onSave,
columnPickerSettings: mockedColumnPickerSettings,
setDynamicColumnSettings,
}),
);
// Act
act(() =>
result.current.handlers.onDynamicColumnSelect([
...mockedColumnPickerSettings,
"attribute:QXR0cmlidXRlOjI3",
]),
);
act(() => result.current.handlers.onToggle("attribute:QXR0cmlidXRlOjI3"));
// Assert
expect(setDynamicColumnSettings).toHaveBeenCalledTimes(1);
expect(onSave).toHaveBeenCalledTimes(1);
expect(result.current.dynamicColumns).toEqual([
...expectedDynamicColumns,
{
@ -317,42 +267,20 @@ describe("useColumns", () => {
selectedColumns: mockedSelectedColumns,
columnCategories: mockedCategories,
onSave,
columnPickerSettings: mockedColumnPickerSettings,
setDynamicColumnSettings,
}),
);
// Act
act(() =>
result.current.handlers.onDynamicColumnSelect([
result.current.handlers.onToggle(
// ABV - which is already selected
"attribute:QXR0cmlidXRlOjIx",
"attribute:QXR0cmlidXRlOjE1",
"attribute:QXR0cmlidXRlOjI5",
"attribute:QXR0cmlidXRlOjE0",
]),
),
);
// Assert
expect(setDynamicColumnSettings).toHaveBeenCalledTimes(1);
expect(onSave).toHaveBeenCalledTimes(1);
expect(result.current.dynamicColumns).toEqual([
{
id: "attribute:QXR0cmlidXRlOjIx",
title: "ABV",
metaGroup: "Attributes",
width: 200,
},
{
id: "attribute:QXR0cmlidXRlOjE1",
title: "Bottle Size",
metaGroup: "Attributes",
width: 200,
},
{
id: "attribute:QXR0cmlidXRlOjI5",
title: "Tag",
metaGroup: "Attributes",
width: 200,
},
{
id: "attribute:QXR0cmlidXRlOjE0",
title: "Color",

View file

@ -2,16 +2,13 @@
import useStateFromProps from "@dashboard/hooks/useStateFromProps";
import { addAtIndex, removeAtIndex } from "@dashboard/utils/lists";
import { GridColumn } from "@glideapps/glide-data-grid";
import difference from "lodash/difference";
import React from "react";
import { AvailableColumn } from "../types";
import {
areCategoriesLoaded,
extractAvailableNodesFromCategories,
extractSelectedNodesFromCategories,
filterSelectedColumns,
mergeCurrentDynamicColumnsWithCandidates,
findDynamicColumn,
mergeSelectedColumns,
sortColumns,
} from "./utils";
@ -34,8 +31,6 @@ export interface UseColumnsProps {
columnCategories?: ColumnCategory[];
selectedColumns: string[];
onSave: (columns: string[]) => void;
columnPickerSettings?: string[];
setDynamicColumnSettings?: (cols: string[]) => void;
}
export const useColumns = ({
@ -43,8 +38,6 @@ export const useColumns = ({
selectedColumns,
columnCategories,
onSave,
columnPickerSettings,
setDynamicColumnSettings,
}: UseColumnsProps) => {
const [dynamicColumns, updateDynamicColumns] = React.useState<
AvailableColumn[] | null | undefined
@ -58,11 +51,11 @@ export const useColumns = ({
updateDynamicColumns(
sortColumns(
extractSelectedNodesFromCategories(columnCategories),
columnPickerSettings,
selectedColumns,
),
);
}
}, [columnCategories, columnPickerSettings, dynamicColumns]);
}, [columnCategories, selectedColumns, dynamicColumns]);
const initialColumnsState = React.useMemo(
() =>
@ -120,47 +113,37 @@ export const useColumns = ({
[setVisibleColumns],
);
const onChange = (columns: string[]) => {
// Recently added is used by datagrid to auto-scroll to the column
setRecentlyAddedColumn(difference(columns, selectedColumns)[0]);
// Saves in LS
onSave(columns);
const onToggle = (columnId: string) => {
const isAdded = !selectedColumns.includes(columnId);
const isDynamic = columnId.includes(":");
if (isAdded) {
onSave([...selectedColumns, columnId]);
setRecentlyAddedColumn(columnId);
} else {
onSave(selectedColumns.filter(id => id !== columnId));
}
if (isDynamic) {
if (isAdded) {
updateDynamicColumns(prevDynamicColumns => [
...(prevDynamicColumns ?? []),
findDynamicColumn(columnCategories, columnId),
]);
} else {
updateDynamicColumns(prevDynamicColumns =>
(prevDynamicColumns ?? []).filter(column => column.id !== columnId),
);
}
}
};
// Should be used only for special cases
const onCustomUpdateVisible = setVisibleColumns;
const onDynamicColumnSelect = (selected: string[]) => {
if (typeof setDynamicColumnSettings !== "function") {
return;
}
// This is optimistic update - dynamic columns are only synced
// with the API on the initial render
setDynamicColumnSettings(selected);
updateDynamicColumns(prevDynamicColumns =>
filterSelectedColumns(
sortColumns(
mergeCurrentDynamicColumnsWithCandidates(
prevDynamicColumns,
filterSelectedColumns(
extractAvailableNodesFromCategories(columnCategories),
selected,
),
),
selected,
),
selected,
),
);
};
return {
handlers: {
onMove,
onResize,
onChange,
onDynamicColumnSelect,
onToggle,
onCustomUpdateVisible,
},
visibleColumns,
@ -168,7 +151,6 @@ export const useColumns = ({
dynamicColumns,
selectedColumns,
columnCategories,
columnPickerSettings,
recentlyAddedColumn,
};
};

View file

@ -1,5 +1,4 @@
import { ArrowLeftIcon, CloseIcon } from "@saleor/macaw-ui/next";
import uniqBy from "lodash/uniqBy";
import React, { Dispatch, SetStateAction } from "react";
import { AvailableColumn } from "../types";
@ -60,11 +59,6 @@ export const sortColumns = (
order: string[],
) => columns?.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
export const filterSelectedColumns = (
columns: AvailableColumn[] | undefined,
selected: string[],
) => columns?.filter(column => selected.includes(column.id));
export const areCategoriesLoaded = (categories: ColumnCategory[] | undefined) =>
categories?.every(category => Array.isArray(category.selectedNodes));
@ -72,9 +66,13 @@ export const extractSelectedNodesFromCategories = (
categories: ColumnCategory[] | undefined,
) => categories?.flatMap(category => category.selectedNodes);
export const extractAvailableNodesFromCategories = (
categories: ColumnCategory[] | undefined,
) => categories?.flatMap(category => category.availableNodes);
export const findDynamicColumn = (
categories: ColumnCategory[],
columnId: string,
) =>
categories
.flatMap(category => category.availableNodes)
.find(column => column?.id === columnId);
export const mergeSelectedColumns = ({
staticColumns,
@ -88,8 +86,3 @@ export const mergeSelectedColumns = ({
[...staticColumns, ...(dynamicColumns ?? [])].filter(
column => selectedColumns.includes(column.id) || column.id === "empty",
);
export const mergeCurrentDynamicColumnsWithCandidates = (
dynamicColumns: AvailableColumn[] | undefined,
candidates: AvailableColumn[],
) => uniqBy([...(dynamicColumns ?? []), ...candidates], "id");

View file

@ -129,7 +129,7 @@ export const OrderDraftListDatagrid = ({
<ColumnPicker
staticColumns={staticColumns}
selectedColumns={selectedColumns}
onSave={handlers.onChange}
onToggle={handlers.onToggle}
/>
)}
/>

View file

@ -130,7 +130,7 @@ export const OrderListDatagrid: React.FC<OrderListDatagridProps> = ({
<ColumnPicker
staticColumns={staticColumns}
selectedColumns={selectedColumns}
onSave={handlers.onChange}
onToggle={handlers.onToggle}
/>
)}
fullScreenTitle={intl.formatMessage(messages.orders)}

View file

@ -69,8 +69,6 @@ interface ProductListDatagridProps
>;
onSelectProductIds: (rowsIndex: number[], clearSelection: () => void) => void;
hasRowHover?: boolean;
columnPickerSettings: string[];
setDynamicColumnSettings: (cols: string[]) => void;
loading: boolean;
}
@ -91,8 +89,6 @@ export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
onSelectProductIds,
hasRowHover,
rowAnchor,
columnPickerSettings,
setDynamicColumnSettings,
}) => {
const intl = useIntl();
const searchProductType = useSearchProductTypes();
@ -149,8 +145,6 @@ export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
}),
selectedColumns: settings.columns,
onSave: handleColumnChange,
columnPickerSettings,
setDynamicColumnSettings,
});
// Logic for updating sort icon in dynamic columns
@ -291,9 +285,7 @@ export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
dynamicColumns={dynamicColumns}
selectedColumns={selectedColumns}
columnCategories={columnCategories}
onDynamicColumnSelect={handlers.onDynamicColumnSelect}
columnPickerSettings={columnPickerSettings}
onSave={handlers.onChange}
onToggle={handlers.onToggle}
/>
)}
/>

View file

@ -66,8 +66,6 @@ const props: ProductListPageProps = {
...pageListProps.default.settings,
columns: ["availability", "productType", "price"],
},
columnPickerSettings: [],
setDynamicColumnSettings: () => undefined,
};
const meta: Meta<typeof ProductListPage> = {

View file

@ -72,8 +72,6 @@ export interface ProductListPageProps
onExport: () => void;
onTabUpdate: (tabName: string) => void;
onTabDelete: (tabIndex: number) => void;
columnPickerSettings: string[];
setDynamicColumnSettings: (cols: string[]) => void;
availableColumnsAttributesOpts: ReturnType<
typeof useAvailableColumnAttributesLazyQuery
>;
@ -111,8 +109,6 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
tabs,
onTabUpdate,
hasPresetsChanged,
columnPickerSettings,
setDynamicColumnSettings,
selectedProductIds,
onProductsDelete,
clearRowSelection,
@ -302,8 +298,6 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
onRowClick={id => {
navigate(productUrl(id));
}}
columnPickerSettings={columnPickerSettings}
setDynamicColumnSettings={setDynamicColumnSettings}
/>
) : (
<ProductListTiles

View file

@ -2,7 +2,6 @@
import { filterable } from "@dashboard/attributes/utils/data";
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 from "@dashboard/components/SaveFilterTabDialog";
import { useShopLimitsQuery } from "@dashboard/components/Shop/queries";
@ -90,9 +89,6 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
ListViews.PRODUCT_LIST,
);
const { columnPickerSettings, setDynamicColumnsSettings } =
useColumnPickerSettings("PRODUCT_LIST");
usePaginationReset(productListUrl, params, settings.rowNumber);
const intl = useIntl();
@ -289,7 +285,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
[params, settings.rowNumber],
);
const filteredColumnIds = (columnPickerSettings ?? [])
const filteredColumnIds = (settings.columns ?? [])
.filter(isAttributeColumnValue)
.map(getAttributeIdFromColumnValue);
@ -420,8 +416,6 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
tabs={presets.map(tab => tab.name)}
onExport={() => openModal("export")}
selectedChannelId={selectedChannel?.id}
columnPickerSettings={columnPickerSettings}
setDynamicColumnSettings={setDynamicColumnsSettings}
selectedProductIds={selectedRowIds}
onSelectProductIds={handleSetSelectedProductIds}
clearRowSelection={clearRowSelection}