Skip to content

Commit bcde132

Browse files
authored
feat(web): add function list context menu (#1825)
* feat(web): add function list context menu * fix popover trigger click * fix delete with new function render & fix contextmenu hide after delete
1 parent 3e17727 commit bcde132

File tree

13 files changed

+222
-49
lines changed

13 files changed

+222
-49
lines changed

web/package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"pako": "^2.1.0",
4949
"qrcode.react": "^3.1.0",
5050
"react": "18.2.0",
51+
"react-contexify": "^6.0.0",
5152
"react-datepicker": "^4.11.0",
5253
"react-day-picker": "^8.8.0",
5354
"react-dom": "18.2.0",
@@ -101,4 +102,4 @@
101102
"*.{ts,tsx,js}": "eslint --fix",
102103
"*.{css,scss}": "stylelint --fix"
103104
}
104-
}
105+
}

web/src/components/ConfirmButton/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface ConfirmButtonProps {
1818
headerText: string;
1919
bodyText: string | React.ReactElement | any;
2020
confirmButtonText?: string;
21+
hideContextMenu?: () => void;
2122
children: React.ReactElement;
2223
}
2324

@@ -26,6 +27,7 @@ const ConfirmButton = ({
2627
headerText,
2728
bodyText,
2829
confirmButtonText,
30+
hideContextMenu,
2931
children,
3032
}: ConfirmButtonProps) => {
3133
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -34,6 +36,7 @@ const ConfirmButton = ({
3436
const onSubmit: React.MouseEventHandler<HTMLButtonElement> = (event) => {
3537
onSuccessAction(event);
3638
onClose();
39+
hideContextMenu && hideContextMenu();
3740
};
3841

3942
return (

web/src/components/DateRangePicker/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { DateRange, DayPicker, SelectRangeEventHandler } from "react-day-picker"
33
import { useTranslation } from "react-i18next";
44
import {
55
Box,
6+
Button,
67
Input,
78
Popover,
89
PopoverContent,
@@ -99,9 +100,9 @@ export default function DateRangePicker(props: {
99100
/>
100101
<Popover onClose={onClose}>
101102
<PopoverTrigger>
102-
<div>
103-
<CalendarIcon className="mr-3 cursor-pointer !text-grayModern-500" fontSize="16" />
104-
</div>
103+
<Button variant="none" px={0} mr={2} minW={4}>
104+
<CalendarIcon className="!text-grayModern-500 pb-[2px]" fontSize="18" />
105+
</Button>
105106
</PopoverTrigger>
106107
<PopoverContent zIndex={99}>
107108
<DayPicker

web/src/components/MoreButton/index.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
Box,
3+
Button,
34
Popover,
45
PopoverContent,
56
PopoverTrigger,
@@ -17,28 +18,26 @@ export default function MoreButton(props: {
1718
label: string;
1819
maxWidth?: string;
1920
className?: string;
21+
refItem?: React.RefObject<HTMLDivElement>;
2022
}) {
21-
const { children, isHidden, maxWidth, label = t("openPopover"), className } = props;
23+
const { children, isHidden, maxWidth, label = t("openPopover"), className, refItem } = props;
2224
const { isOpen, onOpen, onClose } = useDisclosure();
2325
return (
2426
<div className={clsx("flex group-hover:visible ", isHidden ? "invisible" : "visible")}>
2527
<Popover
2628
isOpen={isOpen}
2729
onOpen={onOpen}
2830
onClose={onClose}
29-
closeOnBlur={true}
3031
placement="bottom"
3132
>
3233
<Tooltip aria-label="tooltip" placement="bottom" label={label}>
3334
<Box display="inline-block">
3435
<PopoverTrigger>
35-
<div className="px-1">
36-
<MoreIcon className="cursor-pointer align-middle" fontSize={12} />
37-
</div>
36+
<Button variant="none" p={0} minW={0} h={0} w={5}><MoreIcon className="cursor-pointer align-middle" fontSize={12} /></Button>
3837
</PopoverTrigger>
3938
</Box>
4039
</Tooltip>
41-
<PopoverContent p="2" maxWidth={maxWidth ? maxWidth : "100px"} className={className}>
40+
<PopoverContent p="2" maxWidth={maxWidth ? maxWidth : "120px"} className={className}>
4241
<div className="flex justify-around">{children}</div>
4342
</PopoverContent>
4443
</Popover>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.contexify {
2+
box-shadow: none !important;
3+
border: 1px solid #e2e8f0 !important;
4+
min-width: 0 !important;
5+
justify-content: center;
6+
width: 120px;
7+
padding: 2px !important;
8+
}
9+
10+
.contexify_theme-dark {
11+
background-color: #212630 !important;
12+
border: 1px solid rgba(255, 255, 255, 0.16) !important;
13+
}
14+
15+
.contexify_itemContent {
16+
background: none !important;
17+
width: 100% !important;
18+
height: 100% !important;
19+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Item, Menu } from "react-contexify";
2+
import "./index.scss"
3+
import "react-contexify/dist/ReactContexify.css";
4+
import CreateModal from "../CreateModal";
5+
import { TFunction } from "@/apis/typing";
6+
import IconText from "@/components/IconText";
7+
import { EditIconLine, LinkIcon, RecycleDeleteIcon } from "@/components/CommonIcon";
8+
import { useTranslation } from "react-i18next";
9+
import ConfirmButton from "@/components/ConfirmButton";
10+
import { useDeleteFunctionMutation } from "../../../service";
11+
import useGlobalStore from "@/pages/globalStore";
12+
import CopyText from "@/components/CopyText";
13+
import { COLOR_MODE } from "@/constants";
14+
import { useColorMode } from "@chakra-ui/react";
15+
16+
17+
export default function ContextMenu(props: { functionItem: TFunction, tagsList: string[], hideAll: () => void }) {
18+
const { functionItem, tagsList, hideAll } = props;
19+
const { t } = useTranslation();
20+
const deleteFunctionMutation = useDeleteFunctionMutation();
21+
const { showSuccess, currentApp } = useGlobalStore();
22+
const darkMode = useColorMode().colorMode === COLOR_MODE.dark;
23+
24+
return (
25+
<Menu id={functionItem._id} animation="fade" className="flex" theme={darkMode ? "dark" : "light"}>
26+
<Item closeOnClick={false}>
27+
<CreateModal
28+
functionItem={functionItem} tagList={tagsList} hideContextMenu={hideAll}>
29+
<IconText
30+
icon={
31+
<div className="flex h-5 items-center">
32+
<EditIconLine />
33+
</div>
34+
}
35+
text={t("Edit")}
36+
/>
37+
</CreateModal>
38+
</Item>
39+
<Item>
40+
<ConfirmButton
41+
onSuccessAction={async () => {
42+
const res = await deleteFunctionMutation.mutateAsync(functionItem);
43+
if (!res.error) {
44+
showSuccess(t("DeleteSuccess"));
45+
}
46+
}}
47+
headerText={String(t("Delete"))}
48+
bodyText={String(t("FunctionPanel.DeleteConfirm"))}
49+
hideContextMenu={hideAll}
50+
>
51+
<IconText
52+
icon={<div className="h-5 flex items-center"><RecycleDeleteIcon fontSize={16} /></div>}
53+
text={t("Delete")}
54+
className="hover:!text-error-600"
55+
/>
56+
</ConfirmButton>
57+
</Item>
58+
<Item>
59+
<CopyText
60+
text={`${currentApp.origin}/${functionItem.name}`}
61+
hideToolTip
62+
>
63+
<IconText
64+
icon={<div className="h-5 flex items-center"><LinkIcon fontSize={22} /></div>}
65+
text={t("Copy")}
66+
className="hover:!text-primary-400"
67+
/>
68+
</CopyText>
69+
</Item>
70+
</Menu>
71+
)
72+
}

web/src/pages/app/functions/mods/FunctionPanel/CreateModal/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@ const CreateModal = (props: {
4040
children?: React.ReactElement;
4141
tagList?: any;
4242
aiCode?: string;
43+
hideContextMenu?: () => void;
4344
}) => {
4445
const { isOpen, onOpen, onClose } = useDisclosure();
4546
const { showSuccess, currentApp } = useGlobalStore();
4647

47-
const { functionItem, children = null, tagList, aiCode } = props;
48+
const { functionItem, children = null, tagList, aiCode, hideContextMenu } = props;
4849
const isEdit = !!functionItem;
4950
const navigate = useNavigate();
5051
const [searchKey, setSearchKey] = useState("");
@@ -129,6 +130,7 @@ const CreateModal = (props: {
129130
onClose();
130131
reset(defaultValues);
131132
navigate(`/app/${currentApp.appid}/function/${res.data.name}`, { replace: true });
133+
hideContextMenu && hideContextMenu();
132134
}
133135
};
134136

web/src/pages/app/functions/mods/FunctionPanel/index.tsx

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { cloneDeep } from "lodash";
2020

2121
import {
2222
EditIconLine,
23+
LinkIcon,
2324
RecycleBinIcon,
2425
RecycleDeleteIcon,
2526
TriggerIcon,
@@ -53,6 +54,10 @@ import RecycleBinModal from "@/pages/app/functions/mods/RecycleBinModal";
5354
import useCustomSettingStore from "@/pages/customSetting";
5455
import useGlobalStore from "@/pages/globalStore";
5556

57+
import { useContextMenu } from "react-contexify";
58+
import ContextMenu from "./ContextMenu";
59+
import CopyText from "@/components/CopyText";
60+
5661
type TagItem = {
5762
tagName: string;
5863
selected: boolean;
@@ -114,6 +119,8 @@ export default function FunctionList() {
114119

115120
const [currentTag, setCurrentTag] = useState<TagItem | null>(null);
116121

122+
const { show, hideAll } = useContextMenu();
123+
117124
const generateRoot = useCallback(
118125
(data: TFunction[]) => {
119126
const root = cloneDeep(functionRoot);
@@ -185,9 +192,9 @@ export default function FunctionList() {
185192
return oldTag.length > 0
186193
? oldTag[0]
187194
: {
188-
tagName: tagName,
189-
selected: false,
190-
};
195+
tagName: tagName,
196+
selected: false,
197+
};
191198
});
192199
setTagsList(newTags);
193200

@@ -277,9 +284,8 @@ export default function FunctionList() {
277284
return (
278285
<span className="flex select-none items-center">
279286
<span>{item.desc}</span>
280-
<div className="ml-1 translate-y-[1px] scale-[.85] opacity-75">{` ${
281-
nameParts[nameParts.length - 1]
282-
}`}</div>
287+
<div className="ml-1 translate-y-[1px] scale-[.85] opacity-75">{` ${nameParts[nameParts.length - 1]
288+
}`}</div>
283289
</span>
284290
);
285291
} else {
@@ -321,14 +327,14 @@ export default function FunctionList() {
321327
darkMode ? "text-grayIron-200" : " text-grayIron-700",
322328
item.name === currentFunction?.name && !item.children?.length && "!text-primary-700",
323329
dragOverFuncDir !== null &&
324-
((item.children?.length && (item.name + "/").startsWith(dragOverFuncDir)) ||
325-
(!item.children?.length && item.name.startsWith(dragOverFuncDir))) &&
326-
"!text-primary-700",
330+
((item.children?.length && (item.name + "/").startsWith(dragOverFuncDir)) ||
331+
(!item.children?.length && item.name.startsWith(dragOverFuncDir))) &&
332+
"!text-primary-700",
327333
"!mb-0 pb-[2px]",
328334
dragOverFuncDir !== null &&
329-
((item.children?.length && (item.name + "/").startsWith(dragOverFuncDir)) ||
330-
(!item.children?.length && item.name.startsWith(dragOverFuncDir))) &&
331-
"bg-[#f9f9f9]",
335+
((item.children?.length && (item.name + "/").startsWith(dragOverFuncDir)) ||
336+
(!item.children?.length && item.name.startsWith(dragOverFuncDir))) &&
337+
"bg-[#f9f9f9]",
332338
)}
333339
onClick={() => {
334340
if (!item.children?.length) {
@@ -348,6 +354,22 @@ export default function FunctionList() {
348354
onDragStart={onDragStart}
349355
data-func={item.name}
350356
data-is-func-dir={!!item.children?.length}
357+
onContextMenu={(e) => {
358+
const sidebarWidth = JSON.parse(localStorage.getItem("laf_custom_setting") || "").state.layoutInfo.functionPage.SideBar.style.width || 0
359+
if (!!item.children?.length) return;
360+
if (e.clientX > sidebarWidth - 120) {
361+
show({
362+
event: e,
363+
id: item._id,
364+
position: {
365+
x: e.clientX - 120,
366+
y: e.clientY,
367+
}
368+
})
369+
} else {
370+
show({ event: e, id: item._id })
371+
}
372+
}}
351373
>
352374
<div
353375
className="flex items-center overflow-hidden text-ellipsis whitespace-nowrap font-medium"
@@ -364,8 +386,8 @@ export default function FunctionList() {
364386
<HStack spacing={1}>
365387
{functionCache.getCache(item?._id, (item as any)?.source?.code) !==
366388
(item as any)?.source?.code && (
367-
<span className="mt-[1px] inline-block h-1 w-1 flex-none rounded-full bg-rose-500"></span>
368-
)}
389+
<span className="mt-[1px] inline-block h-1 w-1 flex-none rounded-full bg-rose-500"></span>
390+
)}
369391
<MoreButton isHidden={item.name !== currentFunction?.name} label={t("Operation")}>
370392
<>
371393
<CreateModal functionItem={item} tagList={tagsList}>
@@ -394,11 +416,26 @@ export default function FunctionList() {
394416
className="hover:!text-error-600"
395417
/>
396418
</ConfirmButton>
419+
<CopyText
420+
text={`${currentApp.origin}/${item.name}`}
421+
hideToolTip
422+
>
423+
<IconText
424+
icon={<div className="h-5 flex items-center"><LinkIcon fontSize={22} /></div>}
425+
text={t("Copy")}
426+
className="hover:!text-primary-400"
427+
/>
428+
</CopyText>
397429
</>
398430
</MoreButton>
399431
</HStack>
400432
)}
401433
</SectionList.Item>
434+
{item._id && <ContextMenu
435+
functionItem={item as unknown as TFunction}
436+
tagsList={tagsList.map((item) => item.tagName)}
437+
hideAll={hideAll}
438+
/>}
402439
{item.isExpanded && item?.children?.length && renderSectionItems(item.children)}
403440
</React.Fragment>
404441
);

0 commit comments

Comments
 (0)