@rovula/ui 0.0.47 → 0.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/bundle.css +32 -4
- package/dist/cjs/bundle.js +3 -3
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Switch/Switch.stories.d.ts +1 -6
- package/dist/cjs/types/components/Tree/Tree.d.ts +4 -0
- package/dist/cjs/types/components/Tree/Tree.stories.d.ts +12 -0
- package/dist/cjs/types/components/Tree/TreeItem.d.ts +4 -0
- package/dist/cjs/types/components/Tree/index.d.ts +4 -0
- package/dist/cjs/types/components/Tree/type.d.ts +76 -0
- package/dist/cjs/types/index.d.ts +1 -0
- package/dist/components/Switch/Switch.js +2 -2
- package/dist/components/Switch/Switch.stories.js +2 -7
- package/dist/components/Tree/Tree.js +104 -0
- package/dist/components/Tree/Tree.stories.js +162 -0
- package/dist/components/Tree/TreeItem.js +81 -0
- package/dist/components/Tree/index.js +4 -0
- package/dist/components/Tree/type.js +1 -0
- package/dist/esm/bundle.css +32 -4
- package/dist/esm/bundle.js +1 -1
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Switch/Switch.stories.d.ts +1 -6
- package/dist/esm/types/components/Tree/Tree.d.ts +4 -0
- package/dist/esm/types/components/Tree/Tree.stories.d.ts +12 -0
- package/dist/esm/types/components/Tree/TreeItem.d.ts +4 -0
- package/dist/esm/types/components/Tree/index.d.ts +4 -0
- package/dist/esm/types/components/Tree/type.d.ts +76 -0
- package/dist/esm/types/index.d.ts +1 -0
- package/dist/index.d.ts +82 -2
- package/dist/index.js +1 -0
- package/dist/src/theme/global.css +75 -14
- package/dist/theme/themes/SKL/color.css +10 -10
- package/dist/theme/themes/xspector/baseline.css +1 -0
- package/dist/theme/themes/xspector/components/switch.css +30 -0
- package/package.json +1 -1
- package/src/components/Switch/Switch.stories.tsx +2 -7
- package/src/components/Switch/Switch.tsx +2 -2
- package/src/components/Tree/Tree.stories.tsx +288 -0
- package/src/components/Tree/Tree.tsx +192 -0
- package/src/components/Tree/TreeItem.tsx +231 -0
- package/src/components/Tree/index.ts +5 -0
- package/src/components/Tree/type.ts +90 -0
- package/src/index.ts +1 -0
- package/src/theme/themes/SKL/color.css +10 -10
- package/src/theme/themes/xspector/baseline.css +1 -0
- package/src/theme/themes/xspector/components/switch.css +30 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import Tree from "./Tree";
|
|
4
|
+
import { ActionButton, Icon } from "@/index";
|
|
5
|
+
|
|
6
|
+
const exampleData = [
|
|
7
|
+
{
|
|
8
|
+
id: "1",
|
|
9
|
+
title: "Parent Folder 1",
|
|
10
|
+
children: [
|
|
11
|
+
{
|
|
12
|
+
id: "1.1",
|
|
13
|
+
title: "Child Folder 1.1",
|
|
14
|
+
children: [
|
|
15
|
+
{ id: "1.1.1", title: "Sub Folder 1.1.1" },
|
|
16
|
+
{ id: "1.1.2", title: "Sub Folder 1.1.2" },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
{ id: "1.2", title: "Child Folder 1.2" },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "2",
|
|
24
|
+
title: "Parent Folder 2",
|
|
25
|
+
children: [{ id: "2.1", title: "Child Folder 2.1" }],
|
|
26
|
+
},
|
|
27
|
+
{ id: "3", title: "Parent Folder 3" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const commonProps = {
|
|
31
|
+
defaultExpandedId: ["1", "1.1"],
|
|
32
|
+
defaultCheckedId: ["1.1"],
|
|
33
|
+
defaultExpandAll: true,
|
|
34
|
+
defaultCheckAll: true,
|
|
35
|
+
hierarchicalCheck: true,
|
|
36
|
+
disabled: false,
|
|
37
|
+
showIcon: true,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Storybook metadata
|
|
41
|
+
const meta: Meta<typeof Tree> = {
|
|
42
|
+
title: "Components/Tree",
|
|
43
|
+
component: Tree,
|
|
44
|
+
tags: ["autodocs"],
|
|
45
|
+
parameters: {
|
|
46
|
+
layout: "fullscreen",
|
|
47
|
+
},
|
|
48
|
+
decorators: [
|
|
49
|
+
(Story) => (
|
|
50
|
+
<div className="p-5 flex w-full bg-base-bg">
|
|
51
|
+
<Story />
|
|
52
|
+
</div>
|
|
53
|
+
),
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default meta;
|
|
58
|
+
|
|
59
|
+
// Default story
|
|
60
|
+
export const Default: StoryObj<typeof Tree> = {
|
|
61
|
+
args: {
|
|
62
|
+
data: exampleData,
|
|
63
|
+
...commonProps,
|
|
64
|
+
showIcon: true,
|
|
65
|
+
},
|
|
66
|
+
render: (args) => {
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex flex-row gap-4 w-full">
|
|
69
|
+
<Tree {...args} />
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const onClick: StoryObj<typeof Tree> = {
|
|
76
|
+
args: {
|
|
77
|
+
data: exampleData.map((item) => ({
|
|
78
|
+
...item,
|
|
79
|
+
onClickItem: (id: string) => alert("Click item " + id),
|
|
80
|
+
})),
|
|
81
|
+
...commonProps,
|
|
82
|
+
},
|
|
83
|
+
render: (args) => {
|
|
84
|
+
return (
|
|
85
|
+
<div className="flex flex-row gap-4 w-full">
|
|
86
|
+
<Tree {...args} />
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const CustomIcon: StoryObj<typeof Tree> = {
|
|
93
|
+
args: {
|
|
94
|
+
data: exampleData.map((item, idx) => ({
|
|
95
|
+
...item,
|
|
96
|
+
...(idx === 0 && {
|
|
97
|
+
icon: <Icon name="home" />,
|
|
98
|
+
}),
|
|
99
|
+
...(idx === 1 && {
|
|
100
|
+
renderIcon: ({ expanded, selected }) => (
|
|
101
|
+
<Icon
|
|
102
|
+
name={expanded ? "home" : "home-modern"}
|
|
103
|
+
className={selected ? "fill-info" : "fill-error"}
|
|
104
|
+
/>
|
|
105
|
+
),
|
|
106
|
+
}),
|
|
107
|
+
})),
|
|
108
|
+
...commonProps,
|
|
109
|
+
},
|
|
110
|
+
render: (args) => {
|
|
111
|
+
return (
|
|
112
|
+
<div className="flex flex-row gap-4 w-full">
|
|
113
|
+
<Tree
|
|
114
|
+
{...args}
|
|
115
|
+
renderIcon={({ expanded, selected }) => (
|
|
116
|
+
<Icon
|
|
117
|
+
name={expanded ? "bell" : "bell-slash"}
|
|
118
|
+
className={selected ? "fill-primary" : "fill-secondary"}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const renderRightSection: StoryObj<typeof Tree> = {
|
|
128
|
+
args: {
|
|
129
|
+
data: exampleData,
|
|
130
|
+
...commonProps,
|
|
131
|
+
},
|
|
132
|
+
render: (args) => {
|
|
133
|
+
return (
|
|
134
|
+
<div className="flex flex-row gap-4 w-full">
|
|
135
|
+
<Tree
|
|
136
|
+
{...args}
|
|
137
|
+
renderRightSection={() => (
|
|
138
|
+
<ActionButton variant="icon" onClick={() => alert("Say hi!")}>
|
|
139
|
+
<Icon name="ellipsis-vertical" />
|
|
140
|
+
</ActionButton>
|
|
141
|
+
)}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const ControlShowExpandButton: StoryObj<typeof Tree> = {
|
|
149
|
+
args: {
|
|
150
|
+
data: exampleData.map((item) => ({
|
|
151
|
+
...item,
|
|
152
|
+
showExpandButton: true,
|
|
153
|
+
})),
|
|
154
|
+
},
|
|
155
|
+
render: (args) => {
|
|
156
|
+
return (
|
|
157
|
+
<div className="flex flex-row gap-4 w-full">
|
|
158
|
+
<Tree {...args} />
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const Diabled: StoryObj<typeof Tree> = {
|
|
165
|
+
args: {
|
|
166
|
+
data: exampleData,
|
|
167
|
+
...commonProps,
|
|
168
|
+
disabled: true,
|
|
169
|
+
},
|
|
170
|
+
render: (args) => {
|
|
171
|
+
return (
|
|
172
|
+
<div className="flex flex-row gap-4 w-full">
|
|
173
|
+
<Tree
|
|
174
|
+
{...args}
|
|
175
|
+
renderRightSection={() => (
|
|
176
|
+
<ActionButton variant="icon" onClick={() => alert("Say hi!")}>
|
|
177
|
+
<Icon name="ellipsis-vertical" />
|
|
178
|
+
</ActionButton>
|
|
179
|
+
)}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export const DiabledEachItem: StoryObj<typeof Tree> = {
|
|
187
|
+
args: {
|
|
188
|
+
data: exampleData.map((item, i) => ({ ...item, disabled: i === 0 })),
|
|
189
|
+
...commonProps,
|
|
190
|
+
disabled: undefined,
|
|
191
|
+
},
|
|
192
|
+
render: (args) => {
|
|
193
|
+
return (
|
|
194
|
+
<div className="flex flex-row gap-4 w-full">
|
|
195
|
+
<Tree
|
|
196
|
+
{...args}
|
|
197
|
+
renderRightSection={() => (
|
|
198
|
+
<ActionButton variant="icon" onClick={() => alert("Say hi!")}>
|
|
199
|
+
<Icon name="ellipsis-vertical" />
|
|
200
|
+
</ActionButton>
|
|
201
|
+
)}
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const ExpandLoadData: StoryObj<typeof Tree> = {
|
|
209
|
+
args: {},
|
|
210
|
+
render: (args) => {
|
|
211
|
+
const [data, setData] = useState([
|
|
212
|
+
{
|
|
213
|
+
id: "1",
|
|
214
|
+
title: "Parent Folder 1",
|
|
215
|
+
showExpandButton: true,
|
|
216
|
+
children: [],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "2",
|
|
220
|
+
title: "Parent Folder 2",
|
|
221
|
+
showExpandButton: true,
|
|
222
|
+
children: [],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: "3",
|
|
226
|
+
title: "Parent Folder 3",
|
|
227
|
+
showExpandButton: true,
|
|
228
|
+
children: [],
|
|
229
|
+
},
|
|
230
|
+
]);
|
|
231
|
+
const [loadingId, setLoadingId] = useState<string[]>([]);
|
|
232
|
+
const [loadedId, setLoadedId] = useState<string[]>([]);
|
|
233
|
+
|
|
234
|
+
const updateNode = (nodes: any[], id: string, newChildren: any[]): any[] =>
|
|
235
|
+
nodes.map((node) => {
|
|
236
|
+
if (node.id === id) {
|
|
237
|
+
return { ...node, children: newChildren };
|
|
238
|
+
}
|
|
239
|
+
if (node.children?.length) {
|
|
240
|
+
return {
|
|
241
|
+
...node,
|
|
242
|
+
children: updateNode(node.children, id, newChildren),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return node;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const handleOnExpandChange = (id: string, isExpand: boolean) => {
|
|
249
|
+
// *Note can you other way for improve should load if need with other way without loadedId
|
|
250
|
+
|
|
251
|
+
if (isExpand && !loadingId.includes(id) && !loadedId.includes(id)) {
|
|
252
|
+
setLoadingId((prev) => [...prev, id]);
|
|
253
|
+
setTimeout(() => {
|
|
254
|
+
// Mock child data
|
|
255
|
+
const newChildren = [
|
|
256
|
+
{
|
|
257
|
+
id: Date.now() + "1",
|
|
258
|
+
title: `Child of ${id} - 1`,
|
|
259
|
+
children: [],
|
|
260
|
+
showExpandButton: true,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: Date.now() + "2",
|
|
264
|
+
title: `Child of ${id} - 2`,
|
|
265
|
+
children: [],
|
|
266
|
+
showExpandButton: true,
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
setData((prevData) => updateNode(prevData, id, newChildren));
|
|
271
|
+
setLoadingId((prev) => prev.filter((val) => val !== id));
|
|
272
|
+
setLoadedId((prev) => [...prev, id]);
|
|
273
|
+
}, 1500);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<div className="flex flex-row gap-4 w-full">
|
|
279
|
+
<Tree
|
|
280
|
+
{...args}
|
|
281
|
+
data={data}
|
|
282
|
+
loadingId={loadingId}
|
|
283
|
+
onExpandChange={handleOnExpandChange}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
},
|
|
288
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import React, { FC, useCallback, useEffect, useState } from "react";
|
|
2
|
+
import TreeItem from "./TreeItem";
|
|
3
|
+
import { TreeData, TreeProps } from "./type";
|
|
4
|
+
|
|
5
|
+
const Tree: FC<TreeProps> = ({
|
|
6
|
+
classes,
|
|
7
|
+
data,
|
|
8
|
+
defaultExpandedId = [],
|
|
9
|
+
defaultCheckedId = [],
|
|
10
|
+
checkedId,
|
|
11
|
+
loadingId,
|
|
12
|
+
renderIcon,
|
|
13
|
+
renderRightSection,
|
|
14
|
+
renderElement,
|
|
15
|
+
renderTitle,
|
|
16
|
+
onExpandChange,
|
|
17
|
+
onCheckedChange,
|
|
18
|
+
defaultExpandAll = false,
|
|
19
|
+
defaultCheckAll = false,
|
|
20
|
+
hierarchicalCheck = false,
|
|
21
|
+
showIcon = true,
|
|
22
|
+
disabled,
|
|
23
|
+
enableSeparatorLine = true,
|
|
24
|
+
}) => {
|
|
25
|
+
const [checkedState, setCheckedState] = useState<Record<string, boolean>>({});
|
|
26
|
+
const [expandedState, setExpandedState] = useState<Record<string, boolean>>(
|
|
27
|
+
{}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const traverseTree = (
|
|
31
|
+
nodes: TreeData[],
|
|
32
|
+
callback: (node: TreeData) => void
|
|
33
|
+
) => {
|
|
34
|
+
nodes.forEach((node) => {
|
|
35
|
+
callback(node);
|
|
36
|
+
if (node.children) {
|
|
37
|
+
traverseTree(node.children, callback);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (defaultExpandAll) {
|
|
44
|
+
const allExpanded: Record<string, boolean> = {};
|
|
45
|
+
traverseTree(data, (node) => {
|
|
46
|
+
allExpanded[node.id] = true;
|
|
47
|
+
});
|
|
48
|
+
setExpandedState(allExpanded);
|
|
49
|
+
} else if (defaultExpandedId?.length) {
|
|
50
|
+
const initialExpandedState = defaultExpandedId.reduce((acc, id) => {
|
|
51
|
+
acc[id] = true;
|
|
52
|
+
return acc;
|
|
53
|
+
}, {} as Record<string, boolean>);
|
|
54
|
+
setExpandedState(initialExpandedState);
|
|
55
|
+
}
|
|
56
|
+
}, [data, defaultExpandedId, defaultExpandAll]);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (defaultCheckAll) {
|
|
60
|
+
const allChecked: Record<string, boolean> = {};
|
|
61
|
+
traverseTree(data, (node) => {
|
|
62
|
+
allChecked[node.id] = true;
|
|
63
|
+
});
|
|
64
|
+
setCheckedState(allChecked);
|
|
65
|
+
} else if (!checkedId && defaultCheckedId?.length) {
|
|
66
|
+
const initialCheckedState = defaultCheckedId.reduce((acc, id) => {
|
|
67
|
+
acc[id] = true;
|
|
68
|
+
return acc;
|
|
69
|
+
}, {} as Record<string, boolean>);
|
|
70
|
+
setCheckedState(initialCheckedState);
|
|
71
|
+
}
|
|
72
|
+
}, [data, defaultCheckedId, checkedId, defaultCheckAll]);
|
|
73
|
+
|
|
74
|
+
const handleExpandChange = useCallback(
|
|
75
|
+
(id: string, expanded: boolean) => {
|
|
76
|
+
onExpandChange?.(id, expanded);
|
|
77
|
+
setExpandedState((prev) => ({ ...prev, [id]: expanded }));
|
|
78
|
+
},
|
|
79
|
+
[onExpandChange]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const handleCheckedChange = useCallback(
|
|
83
|
+
(id: string, checked: boolean) => {
|
|
84
|
+
let newState = { ...checkedState, [id]: checked };
|
|
85
|
+
|
|
86
|
+
if (hierarchicalCheck) {
|
|
87
|
+
const updateCheckedState = (
|
|
88
|
+
nodeId: string,
|
|
89
|
+
isChecked: boolean,
|
|
90
|
+
state: Record<string, boolean>
|
|
91
|
+
) => {
|
|
92
|
+
state[nodeId] = isChecked;
|
|
93
|
+
|
|
94
|
+
// Update children recursively
|
|
95
|
+
const updateChildren = (parentId: string, isChecked: boolean) => {
|
|
96
|
+
traverseTree(data, (node) => {
|
|
97
|
+
if (node.id === parentId && node.children) {
|
|
98
|
+
node.children.forEach((child) => {
|
|
99
|
+
state[child.id] = isChecked;
|
|
100
|
+
updateChildren(child.id, isChecked);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Update parents recursively
|
|
107
|
+
const updateParents = (
|
|
108
|
+
childId: string,
|
|
109
|
+
state: Record<string, boolean>
|
|
110
|
+
) => {
|
|
111
|
+
traverseTree(data, (node) => {
|
|
112
|
+
if (node.children?.some((child) => child.id === childId)) {
|
|
113
|
+
const allChildrenChecked = node.children.every(
|
|
114
|
+
(child) => state[child.id]
|
|
115
|
+
);
|
|
116
|
+
state[node.id] = allChildrenChecked;
|
|
117
|
+
updateParents(node.id, state);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
updateChildren(nodeId, isChecked);
|
|
123
|
+
updateParents(nodeId, state);
|
|
124
|
+
|
|
125
|
+
return state;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
newState = updateCheckedState(id, checked, newState);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setCheckedState(newState);
|
|
132
|
+
|
|
133
|
+
if (onCheckedChange) {
|
|
134
|
+
const checkedIds = Object.keys(newState).filter((key) => newState[key]);
|
|
135
|
+
onCheckedChange(checkedIds);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
[checkedState, data, onCheckedChange, hierarchicalCheck]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const checkIsExpanded = useCallback(
|
|
142
|
+
(id: string) => !!expandedState[id],
|
|
143
|
+
[expandedState]
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const checkIsChecked = useCallback(
|
|
147
|
+
(id: string) => {
|
|
148
|
+
if (checkedId) {
|
|
149
|
+
return checkedId.includes(id);
|
|
150
|
+
}
|
|
151
|
+
return !!checkedState[id];
|
|
152
|
+
},
|
|
153
|
+
[checkedId, checkedState]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const checkIsLoading = useCallback(
|
|
157
|
+
(id: string) => {
|
|
158
|
+
if (loadingId) {
|
|
159
|
+
return loadingId.includes(id);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
[loadingId]
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className="w-full">
|
|
167
|
+
{data.map((item, idx) => (
|
|
168
|
+
<TreeItem
|
|
169
|
+
key={item.id}
|
|
170
|
+
classes={classes}
|
|
171
|
+
isFirstLevel
|
|
172
|
+
isLastItem={idx === data.length - 1}
|
|
173
|
+
checkIsExpanded={checkIsExpanded}
|
|
174
|
+
checkIsChecked={checkIsChecked}
|
|
175
|
+
onExpandChange={handleExpandChange}
|
|
176
|
+
onCheckedChange={handleCheckedChange}
|
|
177
|
+
checkIsLoading={checkIsLoading}
|
|
178
|
+
renderIcon={renderIcon}
|
|
179
|
+
renderElement={renderElement}
|
|
180
|
+
renderTitle={renderTitle}
|
|
181
|
+
renderRightSection={renderRightSection}
|
|
182
|
+
enableSeparatorLine={enableSeparatorLine}
|
|
183
|
+
disabled={disabled}
|
|
184
|
+
showIcon={showIcon}
|
|
185
|
+
{...item}
|
|
186
|
+
/>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export default Tree;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { ActionButton, Checkbox, Loading } from "@/index";
|
|
2
|
+
import { cn } from "@/utils/cn";
|
|
3
|
+
import React, { FC, ReactNode, useCallback, useEffect, useMemo } from "react";
|
|
4
|
+
import Icon from "../Icon/Icon";
|
|
5
|
+
import { TreeItemProps } from "./type";
|
|
6
|
+
|
|
7
|
+
const TreeItem: FC<TreeItemProps> = ({
|
|
8
|
+
id,
|
|
9
|
+
title,
|
|
10
|
+
classes,
|
|
11
|
+
children,
|
|
12
|
+
isFirstLevel = false,
|
|
13
|
+
disabled,
|
|
14
|
+
icon,
|
|
15
|
+
showIcon,
|
|
16
|
+
showExpandButton,
|
|
17
|
+
enableSeparatorLine = true,
|
|
18
|
+
isLastItem,
|
|
19
|
+
checkIsExpanded,
|
|
20
|
+
checkIsChecked,
|
|
21
|
+
checkIsLoading,
|
|
22
|
+
onExpandChange,
|
|
23
|
+
onCheckedChange,
|
|
24
|
+
onClickItem,
|
|
25
|
+
renderIcon,
|
|
26
|
+
renderElement,
|
|
27
|
+
renderTitle,
|
|
28
|
+
renderRightSection,
|
|
29
|
+
}) => {
|
|
30
|
+
const isLoading = useMemo(() => checkIsLoading?.(id), [checkIsLoading, id]);
|
|
31
|
+
const isChecked = useMemo(() => checkIsChecked(id), [checkIsChecked, id]);
|
|
32
|
+
const isExpanded = useMemo(() => checkIsExpanded(id), [checkIsExpanded, id]);
|
|
33
|
+
const hasChildren = useMemo(() => !!children?.length, [children]);
|
|
34
|
+
const shouldExpandButton = useMemo(
|
|
35
|
+
() => (showExpandButton !== undefined ? showExpandButton : hasChildren),
|
|
36
|
+
[hasChildren, showExpandButton]
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const handleExpandToggle = useCallback(() => {
|
|
40
|
+
onExpandChange?.(id, !isExpanded);
|
|
41
|
+
}, [id, isExpanded, onExpandChange]);
|
|
42
|
+
|
|
43
|
+
// TODO move to props
|
|
44
|
+
const lineSize = 2;
|
|
45
|
+
const horizontalLineWidth = 4;
|
|
46
|
+
const expandButtonSize = 30;
|
|
47
|
+
const spacing = 2;
|
|
48
|
+
|
|
49
|
+
const styles = {
|
|
50
|
+
branch: {
|
|
51
|
+
height: isLastItem
|
|
52
|
+
? `calc(50% + ${lineSize}px)`
|
|
53
|
+
: `calc(100% + ${lineSize}px)`,
|
|
54
|
+
width: lineSize,
|
|
55
|
+
marginTop: -lineSize,
|
|
56
|
+
borderBottomLeftRadius: lineSize / 2,
|
|
57
|
+
},
|
|
58
|
+
horizontalLine: {
|
|
59
|
+
height: lineSize,
|
|
60
|
+
width:
|
|
61
|
+
lineSize +
|
|
62
|
+
horizontalLineWidth +
|
|
63
|
+
(shouldExpandButton ? 0 : expandButtonSize + spacing),
|
|
64
|
+
marginLeft: -lineSize + 0.1,
|
|
65
|
+
borderBottomLeftRadius: lineSize / 2,
|
|
66
|
+
},
|
|
67
|
+
expandButton: {
|
|
68
|
+
width: expandButtonSize,
|
|
69
|
+
height: expandButtonSize,
|
|
70
|
+
},
|
|
71
|
+
childPadding: {
|
|
72
|
+
paddingLeft: isFirstLevel
|
|
73
|
+
? expandButtonSize / 2 - lineSize / 2
|
|
74
|
+
: expandButtonSize / 2 + horizontalLineWidth - lineSize / 2,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (isExpanded && !isLoading && !hasChildren) {
|
|
80
|
+
handleExpandToggle();
|
|
81
|
+
}
|
|
82
|
+
}, [isLoading, handleExpandToggle]);
|
|
83
|
+
|
|
84
|
+
const handleOnClickItem = useCallback(() => {
|
|
85
|
+
onClickItem?.(id);
|
|
86
|
+
}, [onClickItem, id]);
|
|
87
|
+
|
|
88
|
+
const defaultIcon = (
|
|
89
|
+
<Icon
|
|
90
|
+
name={isExpanded ? "folder-open" : "folder"}
|
|
91
|
+
className="fill-warning"
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const customIcon =
|
|
96
|
+
icon ??
|
|
97
|
+
renderIcon?.({
|
|
98
|
+
id,
|
|
99
|
+
expanded: isExpanded,
|
|
100
|
+
selected: isChecked,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const rightIcon = renderRightSection?.({
|
|
104
|
+
id,
|
|
105
|
+
expanded: isExpanded,
|
|
106
|
+
selected: isChecked,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const titleContent = renderTitle
|
|
110
|
+
? renderTitle({ id, title, expanded: isExpanded, selected: isChecked })
|
|
111
|
+
: title;
|
|
112
|
+
|
|
113
|
+
const elementWrapper = (content: ReactNode) =>
|
|
114
|
+
renderElement
|
|
115
|
+
? renderElement({
|
|
116
|
+
id,
|
|
117
|
+
expanded: isExpanded,
|
|
118
|
+
selected: isChecked,
|
|
119
|
+
children: content,
|
|
120
|
+
styles,
|
|
121
|
+
onClick: handleOnClickItem,
|
|
122
|
+
})
|
|
123
|
+
: content;
|
|
124
|
+
|
|
125
|
+
return elementWrapper(
|
|
126
|
+
<div className={cn("flex flex-row w-full", classes?.elementWrapper)}>
|
|
127
|
+
<div
|
|
128
|
+
className={cn("bg-grey-150", { "h-1/2": isLastItem }, classes?.branch)}
|
|
129
|
+
style={styles.branch}
|
|
130
|
+
/>
|
|
131
|
+
<div className={cn("flex flex-col w-full", classes?.itemWrapper)}>
|
|
132
|
+
<div
|
|
133
|
+
className={cn(
|
|
134
|
+
"flex items-center py-2 min-h-10",
|
|
135
|
+
classes?.itemContainer
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
{!isFirstLevel && (
|
|
139
|
+
<div
|
|
140
|
+
className={cn("bg-grey-150", classes?.horizontalLine)}
|
|
141
|
+
style={styles.horizontalLine}
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
144
|
+
{isFirstLevel && !shouldExpandButton && (
|
|
145
|
+
<div
|
|
146
|
+
className={cn("flex mr-[2px]", classes?.expandButton)}
|
|
147
|
+
style={styles.expandButton}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
{shouldExpandButton && (
|
|
151
|
+
<div
|
|
152
|
+
className={cn("flex mr-[2px]", classes?.expandButton)}
|
|
153
|
+
style={styles.expandButton}
|
|
154
|
+
onClick={!isLoading && handleExpandToggle}
|
|
155
|
+
>
|
|
156
|
+
<ActionButton variant="icon" size="sm">
|
|
157
|
+
{isLoading ? (
|
|
158
|
+
<Loading />
|
|
159
|
+
) : (
|
|
160
|
+
<Icon name={isExpanded ? "chevron-down" : "chevron-right"} />
|
|
161
|
+
)}
|
|
162
|
+
</ActionButton>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
<Checkbox
|
|
166
|
+
id={id}
|
|
167
|
+
className={cn("size-[16pt]", classes?.checkbox)}
|
|
168
|
+
checked={isChecked}
|
|
169
|
+
disabled={disabled}
|
|
170
|
+
onCheckedChange={(newChecked) =>
|
|
171
|
+
onCheckedChange?.(id, newChecked as boolean)
|
|
172
|
+
}
|
|
173
|
+
/>
|
|
174
|
+
<div
|
|
175
|
+
className={cn(
|
|
176
|
+
"ml-2 gap-1 flex flex-1 items-center text-foreground",
|
|
177
|
+
classes?.item
|
|
178
|
+
)}
|
|
179
|
+
onClick={handleOnClickItem}
|
|
180
|
+
>
|
|
181
|
+
{showIcon ? customIcon || defaultIcon : null}
|
|
182
|
+
<div
|
|
183
|
+
className={cn(
|
|
184
|
+
"flex flex-1 cursor-pointer text-subtitle5 text-ellipsis",
|
|
185
|
+
classes?.title
|
|
186
|
+
)}
|
|
187
|
+
>
|
|
188
|
+
{titleContent}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
{rightIcon}
|
|
192
|
+
</div>
|
|
193
|
+
{isExpanded && hasChildren && (
|
|
194
|
+
<div
|
|
195
|
+
className={cn("flex flex-col", classes?.childrenWrapper)}
|
|
196
|
+
style={styles.childPadding}
|
|
197
|
+
>
|
|
198
|
+
{children?.map((child, idx) => (
|
|
199
|
+
<TreeItem
|
|
200
|
+
key={child.id}
|
|
201
|
+
classes={classes}
|
|
202
|
+
isLastItem={idx === children.length - 1}
|
|
203
|
+
checkIsExpanded={checkIsExpanded}
|
|
204
|
+
checkIsChecked={checkIsChecked}
|
|
205
|
+
checkIsLoading={checkIsLoading}
|
|
206
|
+
onExpandChange={onExpandChange}
|
|
207
|
+
onCheckedChange={onCheckedChange}
|
|
208
|
+
renderIcon={renderIcon}
|
|
209
|
+
renderElement={renderElement}
|
|
210
|
+
renderTitle={renderTitle}
|
|
211
|
+
disabled={disabled}
|
|
212
|
+
showIcon={showIcon}
|
|
213
|
+
{...child}
|
|
214
|
+
/>
|
|
215
|
+
))}
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
{enableSeparatorLine && isFirstLevel && !isLastItem && (
|
|
219
|
+
<div
|
|
220
|
+
className={cn(
|
|
221
|
+
"bg-grey-150 w-full h-[2px] rounded",
|
|
222
|
+
classes?.separatorLine
|
|
223
|
+
)}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export default TreeItem;
|