Skip to content

Commit 9b1f4ea

Browse files
authored
Merge pull request #101 from easyops-cn/williamcai/bricks/advanced/workbench-layout-v2/change_the_method_of_adding_components_from_checking_to_dragging_and_clicking
feat(workbench-layout-v2): Change the method of adding components fro…
2 parents fd06c42 + 558ebbf commit 9b1f4ea

File tree

11 files changed

+612
-340
lines changed

11 files changed

+612
-340
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./workbench";
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { UseSingleBrickConf } from "@next-core/types";
2+
import { Layout } from "react-grid-layout";
3+
4+
export type WorkbenchComponent = {
5+
position: Layout;
6+
key: string;
7+
title: string;
8+
style?: React.CSSProperties;
9+
useBrick: UseSingleBrickConf;
10+
};

bricks/advanced/src/table/BrickTable.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { ReactUseMultipleBricks } from "@next-core/react-runtime";
1111
import type { UseSingleBrickConf } from "@next-core/types";
1212
import { wrapBrick } from "@next-core/react-element";
1313
import { StyleProvider, createCache } from "@ant-design/cssinjs";
14-
import { K, NS, locales } from "./i18n.js";
15-
import { useTranslation, initializeReactI18n } from "@next-core/i18n/react";
14+
import { NS, locales } from "./i18n.js";
15+
import { initializeReactI18n } from "@next-core/i18n/react";
1616

1717
import { i18n } from "@next-core/i18n";
1818

@@ -167,8 +167,6 @@ const getCustomComp = (
167167
};
168168

169169
export function BrickTable(props: BrickTableProps): React.ReactElement {
170-
const { t } = useTranslation(NS);
171-
172170
const locale = (i18n.language.split("-")[0] === "zh"
173171
? zhCN
174172
: enUS) as unknown as Locale;
@@ -187,7 +185,6 @@ export function BrickTable(props: BrickTableProps): React.ReactElement {
187185
expandIconColumnIndex,
188186
childrenColumnName,
189187
scroll,
190-
optimizedColumns,
191188
onDelete, // 用于 brick form 中,will be deprecated
192189
ellipsisInfo,
193190
showHeader,
@@ -508,7 +505,11 @@ export function BrickTable(props: BrickTableProps): React.ReactElement {
508505
);
509506

510507
if (props.tableDraggable) {
511-
table = <DndProvider backend={HTML5Backend}>{table}</DndProvider>;
508+
table = (
509+
<DndProvider backend={HTML5Backend} context={window}>
510+
{table}
511+
</DndProvider>
512+
);
512513
}
513514

514515
const cahce = useMemo(() => {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from "react";
2+
import { act, fireEvent, render, screen } from "@testing-library/react";
3+
import "@testing-library/jest-dom";
4+
5+
import { DraggableComponentMenuItem } from "./DraggableComponentMenuItem";
6+
7+
jest.mock("react-dnd", () => ({
8+
useDrag: jest.fn(() => [{}]),
9+
}));
10+
11+
describe("DraggableComponentMenuItem", () => {
12+
it("should work", () => {
13+
const component = {
14+
title: "card-1",
15+
useBrick: {
16+
brick: "div",
17+
properties: {
18+
textContent: "card-1",
19+
},
20+
},
21+
position: {
22+
i: "card-1",
23+
x: 0,
24+
y: 0,
25+
w: 2,
26+
h: 1,
27+
},
28+
key: "card-1",
29+
};
30+
const handleClick = jest.fn();
31+
32+
render(
33+
<DraggableComponentMenuItem component={component} onClick={handleClick} />
34+
);
35+
expect(
36+
screen.getByTestId("draggable-component-menu-item")
37+
).toHaveTextContent(component.title);
38+
39+
// click
40+
expect(handleClick).not.toHaveBeenCalled();
41+
act(() => {
42+
fireEvent.click(screen.getByTestId("draggable-component-menu-item"));
43+
});
44+
expect(handleClick).toHaveBeenCalled();
45+
});
46+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React from "react";
2+
import { wrapBrick } from "@next-core/react-element";
3+
import type {
4+
EoSidebarMenuItem,
5+
EoSidebarMenuItemProps,
6+
} from "@next-bricks/nav/sidebar/sidebar-menu-item";
7+
import { useDrag } from "react-dnd";
8+
9+
import { WorkbenchComponent } from "../interfaces";
10+
11+
const WrappedSidebarMenuItem = wrapBrick<
12+
EoSidebarMenuItem,
13+
EoSidebarMenuItemProps
14+
>("eo-sidebar-menu-item");
15+
16+
export interface DraggableComponentMenuItemProps {
17+
component: WorkbenchComponent;
18+
onClick?(): void;
19+
}
20+
21+
export function DraggableComponentMenuItem(
22+
props: DraggableComponentMenuItemProps
23+
): React.ReactElement {
24+
const { component, onClick } = props;
25+
const { title } = component;
26+
/* istanbul ignore next */
27+
const [{ isDragging }, drag] = useDrag({
28+
type: "component",
29+
item: component,
30+
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
31+
});
32+
33+
return (
34+
<WrappedSidebarMenuItem
35+
icon={{
36+
lib: "antd",
37+
icon: "menu",
38+
}}
39+
title={title}
40+
style={{ opacity: isDragging ? 0.4 : 1 }}
41+
onClick={onClick}
42+
data-testid="draggable-component-menu-item"
43+
ref={drag}
44+
>
45+
{title}
46+
</WrappedSidebarMenuItem>
47+
);
48+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
.component {
2+
height: 100%;
3+
}
4+
5+
.editMask {
6+
position: absolute;
7+
left: 0;
8+
right: 0;
9+
top: 45px;
10+
bottom: 0;
11+
z-index: 10;
12+
}
13+
14+
.deleteIcon {
15+
position: absolute;
16+
right: 15px;
17+
top: 15px;
18+
cursor: pointer;
19+
font-size: 16px;
20+
width: 30px;
21+
height: 30px;
22+
display: flex;
23+
justify-content: center;
24+
align-items: center;
25+
background: var(--color-fill-bg-container-1);
26+
color: var(--color-normal-text);
27+
}
28+
29+
.deleteIcon:hover {
30+
color: var(--color-error);
31+
}
32+
33+
.deleteIcon:active {
34+
color: var(--color-error-active);
35+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React from "react";
2+
import { act, fireEvent, render, screen } from "@testing-library/react";
3+
import "@testing-library/jest-dom";
4+
import { DropTargetHookSpec, DropTargetMonitor, useDrop } from "react-dnd";
5+
6+
import { DroppableComponentLayoutItem } from "./DroppableComponentLayoutItem";
7+
import { WorkbenchComponent } from "../interfaces";
8+
9+
jest.mock("react-dnd", () => ({
10+
useDrag: jest.fn(() => [{}]),
11+
useDrop: jest.fn(() => []),
12+
}));
13+
jest.mock("@next-core/react-runtime", () => ({
14+
ReactUseBrick: jest.fn(({ useBrick }) => (
15+
<div
16+
data-testid="react-use-brick"
17+
data-use-brick={JSON.stringify(useBrick)}
18+
>
19+
Mocked ReactUseBrick
20+
</div>
21+
)),
22+
}));
23+
24+
const mockedUseDrop = useDrop as jest.Mock;
25+
26+
describe("DroppableComponentLayoutItem", () => {
27+
it("should work", () => {
28+
const component = {
29+
title: "card-1",
30+
useBrick: {
31+
brick: "div",
32+
properties: {
33+
textContent: "card-1",
34+
},
35+
},
36+
position: {
37+
i: "card-1",
38+
x: 0,
39+
y: 0,
40+
w: 2,
41+
h: 1,
42+
},
43+
key: "card-1",
44+
};
45+
const handleDrop = jest.fn();
46+
const handleDelete = jest.fn();
47+
48+
render(
49+
<DroppableComponentLayoutItem
50+
component={component}
51+
isEdit
52+
onDrop={handleDrop}
53+
onDelete={handleDelete}
54+
/>
55+
);
56+
expect(screen.getByTestId("react-use-brick").dataset["useBrick"]).toBe(
57+
JSON.stringify(component.useBrick)
58+
);
59+
60+
// drop
61+
act(() => {
62+
(
63+
mockedUseDrop.mock.lastCall?.[0] as DropTargetHookSpec<
64+
WorkbenchComponent,
65+
void,
66+
{
67+
isOver: boolean;
68+
}
69+
>
70+
).drop?.(component, {} as DropTargetMonitor<WorkbenchComponent, void>);
71+
});
72+
expect(handleDrop).toHaveBeenCalledWith(component);
73+
74+
// edit mask click
75+
act(() => {
76+
fireEvent.mouseDown(
77+
screen.getByTestId("droppable-component-layout-item-edit-mask")
78+
);
79+
});
80+
81+
// delete click
82+
expect(handleDelete).not.toHaveBeenCalled();
83+
act(() => {
84+
fireEvent.click(
85+
screen.getByTestId("droppable-component-layout-item-delete")
86+
);
87+
});
88+
expect(handleDelete).toHaveBeenCalled();
89+
});
90+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from "react";
2+
import { ReactUseBrick } from "@next-core/react-runtime";
3+
import { wrapBrick } from "@next-core/react-element";
4+
import type {
5+
GeneralIcon,
6+
GeneralIconProps,
7+
} from "@next-bricks/icons/general-icon";
8+
import { useDrop } from "react-dnd";
9+
10+
import { WorkbenchComponent } from "../interfaces";
11+
12+
import styles from "./DroppableComponentLayoutItem.module.css";
13+
14+
const WrappedIcon = wrapBrick<GeneralIcon, GeneralIconProps>("eo-icon");
15+
16+
export interface DroppableComponentLayoutItemProps {
17+
component: WorkbenchComponent;
18+
isEdit?: boolean;
19+
onDrop?(component: WorkbenchComponent): void;
20+
onDelete?(): void;
21+
}
22+
23+
export function DroppableComponentLayoutItem(
24+
props: DroppableComponentLayoutItemProps
25+
): React.ReactElement {
26+
const { component, isEdit, onDrop, onDelete } = props;
27+
28+
const [, drop] = useDrop({
29+
accept: "component",
30+
drop: (component: WorkbenchComponent) => onDrop?.(component),
31+
});
32+
33+
const handleEditMaskClick = (e: React.MouseEvent) => {
34+
e.preventDefault();
35+
};
36+
37+
const handleDeleteClick = (e: React.MouseEvent) => {
38+
e.preventDefault();
39+
onDelete?.();
40+
};
41+
42+
return (
43+
<div className={styles.component} style={component.style} ref={drop}>
44+
{isEdit && (
45+
<div
46+
className={styles.editMask}
47+
onMouseDown={handleEditMaskClick}
48+
data-testid="droppable-component-layout-item-edit-mask"
49+
/>
50+
)}
51+
<ReactUseBrick useBrick={component.useBrick} />
52+
{isEdit && (
53+
<WrappedIcon
54+
icon="delete"
55+
lib="antd"
56+
className={styles.deleteIcon}
57+
onClick={handleDeleteClick}
58+
data-testid="droppable-component-layout-item-delete"
59+
/>
60+
)}
61+
</div>
62+
);
63+
}

0 commit comments

Comments
 (0)