Datagrid - show icon button when single menu item with icon (#2862)
* RowMenu component * Show first menu item as icon * RowAction tests * Change Edit button * Remove outline from colum picker * Change ghost color * Update enterVariantEditPage test * Update test id in test for edit button * Update editVariant selector in test
This commit is contained in:
parent
e90a2fd757
commit
1d4c4d878e
9 changed files with 294 additions and 31 deletions
|
@ -25,7 +25,7 @@ export const PRODUCT_DETAILS = {
|
||||||
uploadSavedImagesButton: '[data-test-id="upload-images"]',
|
uploadSavedImagesButton: '[data-test-id="upload-images"]',
|
||||||
uploadMediaUrlButton: '[data-test-id="upload-media-url"]',
|
uploadMediaUrlButton: '[data-test-id="upload-media-url"]',
|
||||||
saveUploadUrlButton: '[data-test-id="upload-url-button"]',
|
saveUploadUrlButton: '[data-test-id="upload-url-button"]',
|
||||||
editVariant: '[id="menu-list-grow"]',
|
editVariant: '[data-test-id="row-action-button"]',
|
||||||
firstRowDataGrid: "[data-testid='glide-cell-1-0']",
|
firstRowDataGrid: "[data-testid='glide-cell-1-0']",
|
||||||
dataGridTable: "[data-testid='data-grid-canvas']",
|
dataGridTable: "[data-testid='data-grid-canvas']",
|
||||||
};
|
};
|
||||||
|
|
|
@ -154,8 +154,6 @@ export function enterVariantEditPage() {
|
||||||
cy.get(PRODUCT_DETAILS.dataGridTable)
|
cy.get(PRODUCT_DETAILS.dataGridTable)
|
||||||
.should("be.visible")
|
.should("be.visible")
|
||||||
.wait(1000)
|
.wait(1000)
|
||||||
.get(BUTTON_SELECTORS.showMoreButton)
|
|
||||||
.click()
|
|
||||||
.get(PRODUCT_DETAILS.editVariant)
|
.get(PRODUCT_DETAILS.editVariant)
|
||||||
.click();
|
.click();
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ export interface CardMenuItem {
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
withLoading?: boolean;
|
withLoading?: boolean;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
|
Icon?: React.ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardMenuProps {
|
export interface CardMenuProps {
|
||||||
|
|
|
@ -9,17 +9,18 @@ import DataEditor, {
|
||||||
} from "@glideapps/glide-data-grid";
|
} from "@glideapps/glide-data-grid";
|
||||||
import { Card, CardContent, Typography } from "@material-ui/core";
|
import { Card, CardContent, Typography } from "@material-ui/core";
|
||||||
import { usePreventHistoryBack } from "@saleor/hooks/usePreventHistoryBack";
|
import { usePreventHistoryBack } from "@saleor/hooks/usePreventHistoryBack";
|
||||||
import { MoreHorizontalIcon, useTheme } from "@saleor/macaw-ui";
|
import { useTheme } from "@saleor/macaw-ui";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import range from "lodash/range";
|
import range from "lodash/range";
|
||||||
import throttle from "lodash/throttle";
|
import throttle from "lodash/throttle";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import CardMenu, { CardMenuItem } from "../CardMenu";
|
import { CardMenuItem } from "../CardMenu";
|
||||||
import ColumnPicker from "../ColumnPicker";
|
import ColumnPicker from "../ColumnPicker";
|
||||||
import { FullScreenContainer } from "./FullScreenContainer";
|
import { FullScreenContainer } from "./FullScreenContainer";
|
||||||
import { Header } from "./Header";
|
import { Header } from "./Header";
|
||||||
|
import { RowActions } from "./RowActions";
|
||||||
import useStyles, { useDatagridTheme, useFullScreenStyles } from "./styles";
|
import useStyles, { useDatagridTheme, useFullScreenStyles } from "./styles";
|
||||||
import { AvailableColumn } from "./types";
|
import { AvailableColumn } from "./types";
|
||||||
import useCells from "./useCells";
|
import useCells from "./useCells";
|
||||||
|
@ -280,8 +281,8 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
<div className={classes.columnPicker}>
|
<div className={classes.columnPicker}>
|
||||||
<ColumnPicker
|
<ColumnPicker
|
||||||
IconButtonProps={{
|
IconButtonProps={{
|
||||||
className: classes.columnPickerBtn,
|
className: classes.ghostIcon,
|
||||||
variant: "secondary",
|
variant: "ghost",
|
||||||
hoverOutline: false,
|
hoverOutline: false,
|
||||||
}}
|
}}
|
||||||
availableColumns={availableColumnsChoices}
|
availableColumns={availableColumnsChoices}
|
||||||
|
@ -305,26 +306,10 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
{Array(rowsTotal)
|
{Array(rowsTotal)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, index) => (
|
.map((_, index) => (
|
||||||
<div
|
<RowActions
|
||||||
className={clsx(classes.rowAction, {
|
menuItems={menuItems(index)}
|
||||||
[classes.rowActionSelected]: selection?.rows.hasIndex(
|
disabled={index >= rowsTotal - added.length}
|
||||||
index,
|
/>
|
||||||
),
|
|
||||||
[classes.rowActionScrolledToRight]: scrolledToRight,
|
|
||||||
})}
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
<CardMenu
|
|
||||||
disabled={index >= rowsTotal - added.length}
|
|
||||||
Icon={MoreHorizontalIcon}
|
|
||||||
IconButtonProps={{
|
|
||||||
className: classes.columnPickerBtn,
|
|
||||||
hoverOutline: false,
|
|
||||||
state: "default",
|
|
||||||
}}
|
|
||||||
menuItems={menuItems(index)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
206
src/components/Datagrid/RowActions.test.tsx
Normal file
206
src/components/Datagrid/RowActions.test.tsx
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
import { EditIcon, ThemeProvider } from "@saleor/macaw-ui";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { RowActions } from "./RowActions";
|
||||||
|
|
||||||
|
describe("RowActions", () => {
|
||||||
|
it("should render empty when menu items count equal to 0", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
const { container } = render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<RowActions menuItems={[]} disabled={false} />
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render icon button when only one menu item and has icon props", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<RowActions
|
||||||
|
menuItems={[
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
onSelect: jest.fn(),
|
||||||
|
Icon: <EditIcon data-test-id="edit-icon" />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByTestId("row-action-button")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("edit-icon")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render card meu when only one menu item and has no icon props", () => {
|
||||||
|
// Arrange & Act
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<RowActions
|
||||||
|
menuItems={[
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
onSelect: jest.fn(),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByTestId("show-more-button")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render card meu with multiple items", async () => {
|
||||||
|
// Arrange
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<RowActions
|
||||||
|
menuItems={[
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
onSelect: jest.fn(),
|
||||||
|
testId: "edit-button",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
onSelect: jest.fn(),
|
||||||
|
testId: "delete-button",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Upgrade",
|
||||||
|
onSelect: jest.fn(),
|
||||||
|
testId: "upgrade-button",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByTestId("show-more-button"));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByTestId("show-more-button")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("edit-button")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("delete-button")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("upgrade-button")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fire callback when click on icon button when single menu item with icon props", async () => {
|
||||||
|
// Arrange
|
||||||
|
const onSelectCallback = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<RowActions
|
||||||
|
menuItems={[
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
onSelect: onSelectCallback,
|
||||||
|
Icon: <EditIcon />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await userEvent.click(screen.getByTestId("row-action-button"));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(onSelectCallback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fire callback when click on icon button when multiple menu item", async () => {
|
||||||
|
// Arrange
|
||||||
|
const onIconClickCallback = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<RowActions
|
||||||
|
menuItems={[
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
onSelect: onIconClickCallback,
|
||||||
|
testId: "edit-button",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
onSelect: jest.fn(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Upgrade",
|
||||||
|
onSelect: jest.fn(),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await userEvent.click(screen.getByTestId("show-more-button"));
|
||||||
|
await userEvent.click(screen.getByTestId("edit-button"));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(onIconClickCallback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should disabled show more button when RowAction disabled", async () => {
|
||||||
|
// Arrange & Act
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<RowActions
|
||||||
|
menuItems={[
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
onSelect: jest.fn(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
onSelect: jest.fn(),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByTestId("show-more-button")).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should disabled row action button when RowAction disabled", async () => {
|
||||||
|
// Arrange & Act
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<RowActions
|
||||||
|
menuItems={[
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
onSelect: jest.fn(),
|
||||||
|
Icon: <EditIcon />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByTestId("row-action-button")).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
51
src/components/Datagrid/RowActions.tsx
Normal file
51
src/components/Datagrid/RowActions.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { IconButton, MoreHorizontalIcon } from "@saleor/macaw-ui";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import CardMenu, { CardMenuItem } from "../CardMenu";
|
||||||
|
import useStyles from "./styles";
|
||||||
|
|
||||||
|
interface RowActionsProps {
|
||||||
|
menuItems: CardMenuItem[];
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RowActions = ({ menuItems, disabled }: RowActionsProps) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const hasSingleMenuItem = menuItems.length === 1;
|
||||||
|
const firstMenuItem = menuItems[0];
|
||||||
|
|
||||||
|
const handleIconClick = () => {
|
||||||
|
firstMenuItem.onSelect();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!menuItems.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.rowAction}>
|
||||||
|
{hasSingleMenuItem && firstMenuItem.Icon ? (
|
||||||
|
<IconButton
|
||||||
|
data-test-id="row-action-button"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={handleIconClick}
|
||||||
|
className={classes.ghostIcon}
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
{firstMenuItem.Icon}
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<CardMenu
|
||||||
|
disabled={disabled}
|
||||||
|
Icon={MoreHorizontalIcon}
|
||||||
|
IconButtonProps={{
|
||||||
|
className: classes.ghostIcon,
|
||||||
|
hoverOutline: false,
|
||||||
|
state: "default",
|
||||||
|
}}
|
||||||
|
menuItems={menuItems}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -34,10 +34,8 @@ const useStyles = makeStyles(
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
height: 48,
|
height: 48,
|
||||||
},
|
},
|
||||||
columnPickerBtn: {
|
ghostIcon: {
|
||||||
"&:hover": {
|
color: theme.palette.saleor.main[3],
|
||||||
color: theme.palette.saleor.main[1],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
portal: {
|
portal: {
|
||||||
"& input::-webkit-outer-spin-button, input::-webkit-inner-spin-button": {
|
"& input::-webkit-outer-spin-button, input::-webkit-inner-spin-button": {
|
||||||
|
|
22
src/icons/Edit.tsx
Normal file
22
src/icons/Edit.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const EditIcon = () => (
|
||||||
|
<svg
|
||||||
|
width="15"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 15 14"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M13.1791 0.678585C12.5282 0.0277112 11.4729 0.0277113 10.822 0.678585L10.0897 1.41088L13.036 4.35716L13.7683 3.62486C14.4192 2.97399 14.4192 1.91871 13.7683 1.26784L13.1791 0.678585Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11.8575 5.53567L8.91123 2.58939L1.04876 10.4519C0.892484 10.6081 0.804688 10.8201 0.804688 11.0411V12.8089C0.804688 13.2691 1.17778 13.6422 1.63802 13.6422H3.40579C3.6268 13.6422 3.83876 13.5544 3.99504 13.3981L11.8575 5.53567Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default EditIcon;
|
|
@ -12,6 +12,7 @@ import {
|
||||||
RefreshLimitsQuery,
|
RefreshLimitsQuery,
|
||||||
WarehouseFragment,
|
WarehouseFragment,
|
||||||
} from "@saleor/graphql";
|
} from "@saleor/graphql";
|
||||||
|
import EditIcon from "@saleor/icons/Edit";
|
||||||
import { buttonMessages } from "@saleor/intl";
|
import { buttonMessages } from "@saleor/intl";
|
||||||
import { Button } from "@saleor/macaw-ui";
|
import { Button } from "@saleor/macaw-ui";
|
||||||
import { ProductVariantListError } from "@saleor/products/views/ProductUpdate/handlers/errors";
|
import { ProductVariantListError } from "@saleor/products/views/ProductUpdate/handlers/errors";
|
||||||
|
@ -121,6 +122,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = ({
|
||||||
{
|
{
|
||||||
label: "Edit Variant",
|
label: "Edit Variant",
|
||||||
onSelect: () => onRowClick(variants[index].id),
|
onSelect: () => onRowClick(variants[index].id),
|
||||||
|
Icon: <EditIcon />,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
rows={variants?.length ?? 0}
|
rows={variants?.length ?? 0}
|
||||||
|
|
Loading…
Reference in a new issue