From bcde132fd872be9ef033d059055b92d5e20c5fff Mon Sep 17 00:00:00 2001 From: heheer <71265218+newfish-cmyk@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:21:21 +0800 Subject: [PATCH] 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 --- web/package-lock.json | 21 ++++++ web/package.json | 3 +- web/src/components/ConfirmButton/index.tsx | 3 + web/src/components/DateRangePicker/index.tsx | 7 +- web/src/components/MoreButton/index.tsx | 11 ++- .../mods/FunctionPanel/ContextMenu/index.scss | 19 +++++ .../mods/FunctionPanel/ContextMenu/index.tsx | 72 +++++++++++++++++++ .../mods/FunctionPanel/CreateModal/index.tsx | 4 +- .../functions/mods/FunctionPanel/index.tsx | 65 +++++++++++++---- web/src/pages/app/functions/service.ts | 5 +- .../UserSetting/BillingDetails/index.tsx | 21 ++++-- .../UserSetting/RechargeHistory/index.tsx | 17 +++-- .../pages/app/setting/UserSetting/index.tsx | 23 +++--- 13 files changed, 222 insertions(+), 49 deletions(-) create mode 100644 web/src/pages/app/functions/mods/FunctionPanel/ContextMenu/index.scss create mode 100644 web/src/pages/app/functions/mods/FunctionPanel/ContextMenu/index.tsx diff --git a/web/package-lock.json b/web/package-lock.json index 1d3b9dd10b..7623828ef1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -43,6 +43,7 @@ "pako": "^2.1.0", "qrcode.react": "^3.1.0", "react": "18.2.0", + "react-contexify": "^6.0.0", "react-datepicker": "^4.11.0", "react-day-picker": "^8.8.0", "react-dom": "18.2.0", @@ -9387,6 +9388,18 @@ "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-contexify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-contexify/-/react-contexify-6.0.0.tgz", + "integrity": "sha512-jMhz6yZI81Jv3UDj7TXqCkhdkCFEEmvwGCPXsQuA2ZUC8EbCuVQ6Cy8FzKMXa0y454XTDClBN2YFvvmoFlrFkg==", + "dependencies": { + "clsx": "^1.2.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-datepicker": { "version": "4.11.0", "resolved": "https://registry.npmmirror.com/react-datepicker/-/react-datepicker-4.11.0.tgz", @@ -19068,6 +19081,14 @@ "@babel/runtime": "^7.12.13" } }, + "react-contexify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-contexify/-/react-contexify-6.0.0.tgz", + "integrity": "sha512-jMhz6yZI81Jv3UDj7TXqCkhdkCFEEmvwGCPXsQuA2ZUC8EbCuVQ6Cy8FzKMXa0y454XTDClBN2YFvvmoFlrFkg==", + "requires": { + "clsx": "^1.2.1" + } + }, "react-datepicker": { "version": "4.11.0", "resolved": "https://registry.npmmirror.com/react-datepicker/-/react-datepicker-4.11.0.tgz", diff --git a/web/package.json b/web/package.json index 277fde0951..47ee9a0c27 100644 --- a/web/package.json +++ b/web/package.json @@ -48,6 +48,7 @@ "pako": "^2.1.0", "qrcode.react": "^3.1.0", "react": "18.2.0", + "react-contexify": "^6.0.0", "react-datepicker": "^4.11.0", "react-day-picker": "^8.8.0", "react-dom": "18.2.0", @@ -101,4 +102,4 @@ "*.{ts,tsx,js}": "eslint --fix", "*.{css,scss}": "stylelint --fix" } -} \ No newline at end of file +} diff --git a/web/src/components/ConfirmButton/index.tsx b/web/src/components/ConfirmButton/index.tsx index 79a3d89aaf..18c9efbccf 100644 --- a/web/src/components/ConfirmButton/index.tsx +++ b/web/src/components/ConfirmButton/index.tsx @@ -18,6 +18,7 @@ interface ConfirmButtonProps { headerText: string; bodyText: string | React.ReactElement | any; confirmButtonText?: string; + hideContextMenu?: () => void; children: React.ReactElement; } @@ -26,6 +27,7 @@ const ConfirmButton = ({ headerText, bodyText, confirmButtonText, + hideContextMenu, children, }: ConfirmButtonProps) => { const { isOpen, onOpen, onClose } = useDisclosure(); @@ -34,6 +36,7 @@ const ConfirmButton = ({ const onSubmit: React.MouseEventHandler = (event) => { onSuccessAction(event); onClose(); + hideContextMenu && hideContextMenu(); }; return ( diff --git a/web/src/components/DateRangePicker/index.tsx b/web/src/components/DateRangePicker/index.tsx index ae06fa5260..46dad9efc8 100644 --- a/web/src/components/DateRangePicker/index.tsx +++ b/web/src/components/DateRangePicker/index.tsx @@ -3,6 +3,7 @@ import { DateRange, DayPicker, SelectRangeEventHandler } from "react-day-picker" import { useTranslation } from "react-i18next"; import { Box, + Button, Input, Popover, PopoverContent, @@ -99,9 +100,9 @@ export default function DateRangePicker(props: { /> -
- -
+
; }) { - const { children, isHidden, maxWidth, label = t("openPopover"), className } = props; + const { children, isHidden, maxWidth, label = t("openPopover"), className, refItem } = props; const { isOpen, onOpen, onClose } = useDisclosure(); return (
@@ -26,19 +28,16 @@ export default function MoreButton(props: { isOpen={isOpen} onOpen={onOpen} onClose={onClose} - closeOnBlur={true} placement="bottom" > -
- -
+
- +
{children}
diff --git a/web/src/pages/app/functions/mods/FunctionPanel/ContextMenu/index.scss b/web/src/pages/app/functions/mods/FunctionPanel/ContextMenu/index.scss new file mode 100644 index 0000000000..f00342d3c8 --- /dev/null +++ b/web/src/pages/app/functions/mods/FunctionPanel/ContextMenu/index.scss @@ -0,0 +1,19 @@ +.contexify { + box-shadow: none !important; + border: 1px solid #e2e8f0 !important; + min-width: 0 !important; + justify-content: center; + width: 120px; + padding: 2px !important; +} + +.contexify_theme-dark { + background-color: #212630 !important; + border: 1px solid rgba(255, 255, 255, 0.16) !important; +} + +.contexify_itemContent { + background: none !important; + width: 100% !important; + height: 100% !important; +} \ No newline at end of file diff --git a/web/src/pages/app/functions/mods/FunctionPanel/ContextMenu/index.tsx b/web/src/pages/app/functions/mods/FunctionPanel/ContextMenu/index.tsx new file mode 100644 index 0000000000..2221c0cf9a --- /dev/null +++ b/web/src/pages/app/functions/mods/FunctionPanel/ContextMenu/index.tsx @@ -0,0 +1,72 @@ +import { Item, Menu } from "react-contexify"; +import "./index.scss" +import "react-contexify/dist/ReactContexify.css"; +import CreateModal from "../CreateModal"; +import { TFunction } from "@/apis/typing"; +import IconText from "@/components/IconText"; +import { EditIconLine, LinkIcon, RecycleDeleteIcon } from "@/components/CommonIcon"; +import { useTranslation } from "react-i18next"; +import ConfirmButton from "@/components/ConfirmButton"; +import { useDeleteFunctionMutation } from "../../../service"; +import useGlobalStore from "@/pages/globalStore"; +import CopyText from "@/components/CopyText"; +import { COLOR_MODE } from "@/constants"; +import { useColorMode } from "@chakra-ui/react"; + + +export default function ContextMenu(props: { functionItem: TFunction, tagsList: string[], hideAll: () => void }) { + const { functionItem, tagsList, hideAll } = props; + const { t } = useTranslation(); + const deleteFunctionMutation = useDeleteFunctionMutation(); + const { showSuccess, currentApp } = useGlobalStore(); + const darkMode = useColorMode().colorMode === COLOR_MODE.dark; + + return ( + + + + + +
+ } + text={t("Edit")} + /> + + + + { + const res = await deleteFunctionMutation.mutateAsync(functionItem); + if (!res.error) { + showSuccess(t("DeleteSuccess")); + } + }} + headerText={String(t("Delete"))} + bodyText={String(t("FunctionPanel.DeleteConfirm"))} + hideContextMenu={hideAll} + > + } + text={t("Delete")} + className="hover:!text-error-600" + /> + + + + + } + text={t("Copy")} + className="hover:!text-primary-400" + /> + + + + ) +} \ No newline at end of file diff --git a/web/src/pages/app/functions/mods/FunctionPanel/CreateModal/index.tsx b/web/src/pages/app/functions/mods/FunctionPanel/CreateModal/index.tsx index b0002eb4f6..950697ee0f 100644 --- a/web/src/pages/app/functions/mods/FunctionPanel/CreateModal/index.tsx +++ b/web/src/pages/app/functions/mods/FunctionPanel/CreateModal/index.tsx @@ -40,11 +40,12 @@ const CreateModal = (props: { children?: React.ReactElement; tagList?: any; aiCode?: string; + hideContextMenu?: () => void; }) => { const { isOpen, onOpen, onClose } = useDisclosure(); const { showSuccess, currentApp } = useGlobalStore(); - const { functionItem, children = null, tagList, aiCode } = props; + const { functionItem, children = null, tagList, aiCode, hideContextMenu } = props; const isEdit = !!functionItem; const navigate = useNavigate(); const [searchKey, setSearchKey] = useState(""); @@ -129,6 +130,7 @@ const CreateModal = (props: { onClose(); reset(defaultValues); navigate(`/app/${currentApp.appid}/function/${res.data.name}`, { replace: true }); + hideContextMenu && hideContextMenu(); } }; diff --git a/web/src/pages/app/functions/mods/FunctionPanel/index.tsx b/web/src/pages/app/functions/mods/FunctionPanel/index.tsx index 5e638179f2..9a466ef3e6 100644 --- a/web/src/pages/app/functions/mods/FunctionPanel/index.tsx +++ b/web/src/pages/app/functions/mods/FunctionPanel/index.tsx @@ -20,6 +20,7 @@ import { cloneDeep } from "lodash"; import { EditIconLine, + LinkIcon, RecycleBinIcon, RecycleDeleteIcon, TriggerIcon, @@ -53,6 +54,10 @@ import RecycleBinModal from "@/pages/app/functions/mods/RecycleBinModal"; import useCustomSettingStore from "@/pages/customSetting"; import useGlobalStore from "@/pages/globalStore"; +import { useContextMenu } from "react-contexify"; +import ContextMenu from "./ContextMenu"; +import CopyText from "@/components/CopyText"; + type TagItem = { tagName: string; selected: boolean; @@ -114,6 +119,8 @@ export default function FunctionList() { const [currentTag, setCurrentTag] = useState(null); + const { show, hideAll } = useContextMenu(); + const generateRoot = useCallback( (data: TFunction[]) => { const root = cloneDeep(functionRoot); @@ -185,9 +192,9 @@ export default function FunctionList() { return oldTag.length > 0 ? oldTag[0] : { - tagName: tagName, - selected: false, - }; + tagName: tagName, + selected: false, + }; }); setTagsList(newTags); @@ -277,9 +284,8 @@ export default function FunctionList() { return ( {item.desc} -
{` ${ - nameParts[nameParts.length - 1] - }`}
+
{` ${nameParts[nameParts.length - 1] + }`}
); } else { @@ -321,14 +327,14 @@ export default function FunctionList() { darkMode ? "text-grayIron-200" : " text-grayIron-700", item.name === currentFunction?.name && !item.children?.length && "!text-primary-700", dragOverFuncDir !== null && - ((item.children?.length && (item.name + "/").startsWith(dragOverFuncDir)) || - (!item.children?.length && item.name.startsWith(dragOverFuncDir))) && - "!text-primary-700", + ((item.children?.length && (item.name + "/").startsWith(dragOverFuncDir)) || + (!item.children?.length && item.name.startsWith(dragOverFuncDir))) && + "!text-primary-700", "!mb-0 pb-[2px]", dragOverFuncDir !== null && - ((item.children?.length && (item.name + "/").startsWith(dragOverFuncDir)) || - (!item.children?.length && item.name.startsWith(dragOverFuncDir))) && - "bg-[#f9f9f9]", + ((item.children?.length && (item.name + "/").startsWith(dragOverFuncDir)) || + (!item.children?.length && item.name.startsWith(dragOverFuncDir))) && + "bg-[#f9f9f9]", )} onClick={() => { if (!item.children?.length) { @@ -348,6 +354,22 @@ export default function FunctionList() { onDragStart={onDragStart} data-func={item.name} data-is-func-dir={!!item.children?.length} + onContextMenu={(e) => { + const sidebarWidth = JSON.parse(localStorage.getItem("laf_custom_setting") || "").state.layoutInfo.functionPage.SideBar.style.width || 0 + if (!!item.children?.length) return; + if (e.clientX > sidebarWidth - 120) { + show({ + event: e, + id: item._id, + position: { + x: e.clientX - 120, + y: e.clientY, + } + }) + } else { + show({ event: e, id: item._id }) + } + }} >
{functionCache.getCache(item?._id, (item as any)?.source?.code) !== (item as any)?.source?.code && ( - - )} + + )} <> @@ -394,11 +416,26 @@ export default function FunctionList() { className="hover:!text-error-600" /> + +
} + text={t("Copy")} + className="hover:!text-primary-400" + /> + )} + {item._id && item.tagName)} + hideAll={hideAll} + />} {item.isExpanded && item?.children?.length && renderSectionItems(item.children)} ); diff --git a/web/src/pages/app/functions/service.ts b/web/src/pages/app/functions/service.ts index fe509eee86..0294b53a72 100644 --- a/web/src/pages/app/functions/service.ts +++ b/web/src/pages/app/functions/service.ts @@ -135,7 +135,10 @@ export const useDeleteFunctionMutation = () => { onSuccess(data: any) { if (!data.error) { queryClient.invalidateQueries(queryKeys.useFunctionListQuery); - store.setCurrentFunction({}); + if (store.currentFunction?._id === data.data._id) { + const newFunction = store.recentFunctionList[0] || store.allFunctionList[0] || {}; + store.setCurrentFunction(newFunction); + } store.setRecentFunctionList( store.recentFunctionList.filter((item) => item._id !== data.data._id), ); diff --git a/web/src/pages/app/setting/UserSetting/BillingDetails/index.tsx b/web/src/pages/app/setting/UserSetting/BillingDetails/index.tsx index 4a00e014a0..1fed92076c 100644 --- a/web/src/pages/app/setting/UserSetting/BillingDetails/index.tsx +++ b/web/src/pages/app/setting/UserSetting/BillingDetails/index.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { DateRange, DayPicker, SelectRangeEventHandler } from "react-day-picker"; import { useTranslation } from "react-i18next"; import { + Button, Center, Popover, PopoverBody, @@ -102,7 +103,9 @@ export default function BillingDetails() { - + @@ -150,10 +153,12 @@ export default function BillingDetails() { - + @@ -226,11 +231,13 @@ export default function BillingDetails() { - + - + {STATE_LIST.map((item) => ( - + @@ -136,11 +139,13 @@ export default function RechargeHistory() { - + - + {STATE_LIST.map((item) => ( - +