From f108ea8d241750b448bd98a4474501ab8bc7a951 Mon Sep 17 00:00:00 2001 From: 0fatal <72899968+0fatal@users.noreply.github.com> Date: Tue, 2 Jan 2024 16:28:21 +0800 Subject: [PATCH] feat(web): support rename functions using dragging (#1762) * feat(server): ban / in the begin or end of the func name * feat(web): support rename functions using dragging Co-authored-by: maslow --- .../dto/create-function-template.dto.ts | 3 +- .../src/function/dto/create-function.dto.ts | 2 +- .../src/function/dto/update-function.dto.ts | 2 +- .../dto/get-recycle-bin-functions.dto.ts | 2 +- web/public/locales/en/translation.json | 11 +- web/public/locales/zh-CN/translation.json | 10 +- web/public/locales/zh/translation.json | 11 +- web/src/components/FileTypeIcon/index.tsx | 2 +- web/src/components/SectionList/index.tsx | 40 +- .../mods/FunctionPanel/CreateModal/index.tsx | 2 +- .../functions/mods/FunctionPanel/index.tsx | 442 +++++++++++++++--- web/src/pages/app/functions/store.ts | 6 +- .../Mods/AddFunctionModal.tsx | 2 +- 13 files changed, 422 insertions(+), 113 deletions(-) diff --git a/server/src/function-template/dto/create-function-template.dto.ts b/server/src/function-template/dto/create-function-template.dto.ts index 57fcd840b5..a43c044da1 100644 --- a/server/src/function-template/dto/create-function-template.dto.ts +++ b/server/src/function-template/dto/create-function-template.dto.ts @@ -21,8 +21,7 @@ export class FunctionTemplateItemDto { description: 'FunctionTemplate item name', }) @IsNotEmpty() - @MaxLength(48) - @Matches(/^[a-zA-Z0-9_.\-\/]{1,256}$/) + @Matches(/^[a-zA-Z0-9_.\-](?:[a-zA-Z0-9_.\-/]{0,254}[a-zA-Z0-9_.\-])?$/) name: string @ApiPropertyOptional() diff --git a/server/src/function/dto/create-function.dto.ts b/server/src/function/dto/create-function.dto.ts index 5a1923c0f4..79aa4aa7db 100644 --- a/server/src/function/dto/create-function.dto.ts +++ b/server/src/function/dto/create-function.dto.ts @@ -15,7 +15,7 @@ export class CreateFunctionDto { description: 'Function name is unique in the application', }) @IsNotEmpty() - @Matches(/^[a-zA-Z0-9_.\-\/]{1,256}$/) + @Matches(/^[a-zA-Z0-9_.\-](?:[a-zA-Z0-9_.\-/]{0,254}[a-zA-Z0-9_.\-])?$/) name: string @ApiPropertyOptional() diff --git a/server/src/function/dto/update-function.dto.ts b/server/src/function/dto/update-function.dto.ts index a784f2a0c5..b90a4f4339 100644 --- a/server/src/function/dto/update-function.dto.ts +++ b/server/src/function/dto/update-function.dto.ts @@ -16,7 +16,7 @@ export class UpdateFunctionDto { description: 'Function name is unique in the application', }) @IsOptional() - @Matches(/^[a-zA-Z0-9_.\-\/]{1,256}$/) + @Matches(/^[a-zA-Z0-9_.\-](?:[a-zA-Z0-9_.\-/]{0,254}[a-zA-Z0-9_.\-])?$/) newName?: string @ApiPropertyOptional() diff --git a/server/src/recycle-bin/cloud-function/dto/get-recycle-bin-functions.dto.ts b/server/src/recycle-bin/cloud-function/dto/get-recycle-bin-functions.dto.ts index ac3bfeb1f2..c8c89900a5 100644 --- a/server/src/recycle-bin/cloud-function/dto/get-recycle-bin-functions.dto.ts +++ b/server/src/recycle-bin/cloud-function/dto/get-recycle-bin-functions.dto.ts @@ -41,7 +41,7 @@ export class FunctionRecycleBinItemsDto { description: 'Function name is unique in the application', }) @IsNotEmpty() - @Matches(/^[a-zA-Z0-9_.\-\/]{1,256}$/) + @Matches(/^[a-zA-Z0-9_.\-](?:[a-zA-Z0-9_.\-/]{0,254}[a-zA-Z0-9_.\-])?$/) name: string @ApiProperty({ type: CloudFunctionSourceDto }) diff --git a/web/public/locales/en/translation.json b/web/public/locales/en/translation.json index 3f8fd69455..fab94637fd 100644 --- a/web/public/locales/en/translation.json +++ b/web/public/locales/en/translation.json @@ -119,7 +119,7 @@ "SystemDependence": "Built-In", "Value": "Value", "isSupport": "Whether to support", - "FunctionNameRule": "Function names must consist of letters, numbers, periods (.), and hyphens (-), matching the regex: /^[a-zA-Z0-9.-]{1,128}$/.", + "FunctionNameRule": "Function names must consist of letters, numbers, periods (.), and hyphens (-), matching the regex: /^[a-zA-Z0-9_.\\-](?:[a-zA-Z0-9_.\\-/]{0,254}[a-zA-Z0-9_.\\-])?$/.", "EmptyDebugTip": "No results yet", "EmptyFunctionTip": "You have not created the function", "UploadButton": "upload", @@ -135,7 +135,10 @@ "HistoryTips": "No historical versions available", "NoDesc": "No Description", "CurrentVersion": "Current Version", - "HistoryVersion": "History version" + "HistoryVersion": "History version", + "MoveFunctionTip": "Are you sure to move {{srcFunc}} to {{targetDir}} directory?", + "MoveFunctionToRootTip": "Are you sure to move {{srcFunc}} to the root directory?", + "MovingFunction": "Moving functions..." }, "HomePanel": { "APP": "Android or iOS app", @@ -702,8 +705,10 @@ }, "Deprecated": "Deprecated", "All": "All", + "MoveFunction": "Move function", "UpgradeVersionTip": { "Title": "Laf is ready to update!", "Description": "Click to update" } -} \ No newline at end of file +} + diff --git a/web/public/locales/zh-CN/translation.json b/web/public/locales/zh-CN/translation.json index 3ff544ed72..7199f7bfc3 100644 --- a/web/public/locales/zh-CN/translation.json +++ b/web/public/locales/zh-CN/translation.json @@ -119,7 +119,7 @@ "SystemDependence": "内置依赖", "Value": "值", "isSupport": "是否支持", - "FunctionNameRule": "函数名须由字母、数字、点(.)和划线(-_)组成,匹配正则:/^[a-zA-Z0-9_.-/]{1,256}$/", + "FunctionNameRule": "函数名须由字母、数字、点(.)和划线(-_)组成,匹配正则:/^[a-zA-Z0-9_.\\-](?:[a-zA-Z0-9_.\\-/]{0,254}[a-zA-Z0-9_.\\-])?$/", "EmptyDebugTip": "暂无运行结果", "EmptyFunctionTip": "您还没有创建函数", "UploadButton": "上传", @@ -135,7 +135,10 @@ "HistoryTips": "暂无历史版本", "NoDesc": "暂无描述", "CurrentVersion": "当前版本", - "HistoryVersion": "历史版本" + "HistoryVersion": "历史版本", + "MoveFunctionTip": "是否要将 {{srcFunc}} 移动到 {{targetDir}} 目录?", + "MoveFunctionToRootTip": "是否要将 {{srcFunc}} 移动到根目录?", + "MovingFunction": "移动函数中..." }, "HomePanel": { "APP": "Android or iOS 应用", @@ -702,8 +705,9 @@ }, "Deprecated": "已弃用", "All": "全部", + "MoveFunction": "移动函数", "UpgradeVersionTip": { "Title": "Laf 新版本已经准备好了!", "Description": "点击立即更新" } -} \ No newline at end of file +} diff --git a/web/public/locales/zh/translation.json b/web/public/locales/zh/translation.json index c45b3eea66..7b5d1778f4 100644 --- a/web/public/locales/zh/translation.json +++ b/web/public/locales/zh/translation.json @@ -119,7 +119,7 @@ "SystemDependence": "内置依赖", "Value": "值", "isSupport": "是否支持", - "FunctionNameRule": "函数名须由字母、数字、点(.)和划线(-_)组成,匹配正则:/^[a-zA-Z0-9_.-/]{1,256}$/", + "FunctionNameRule": "函数名须由字母、数字、点(.)和划线(-_)组成,匹配正则:/^[a-zA-Z0-9_.\\-](?:[a-zA-Z0-9_.\\-/]{0,254}[a-zA-Z0-9_.\\-])?$/", "EmptyDebugTip": "暂无运行结果", "EmptyFunctionTip": "您还没有创建函数", "UploadButton": "上传", @@ -135,7 +135,10 @@ "HistoryTips": "暂无历史版本", "NoDesc": "暂无描述", "CurrentVersion": "当前版本", - "HistoryVersion": "历史版本" + "HistoryVersion": "历史版本", + "MoveFunctionTip": "是否要将 {{srcFunc}} 移动到 {{targetDir}} 目录?", + "MoveFunctionToRootTip": "是否要将 {{srcFunc}} 移动到根目录?", + "MovingFunction": "移动函数中..." }, "HomePanel": { "APP": "Android or iOS 应用", @@ -702,8 +705,10 @@ }, "Deprecated": "已弃用", "All": "全部", + "MoveFunction": "移动函数", "UpgradeVersionTip": { "Title": "Laf 新版本已经准备好了!", "Description": "点击立即更新" } -} \ No newline at end of file +} + diff --git a/web/src/components/FileTypeIcon/index.tsx b/web/src/components/FileTypeIcon/index.tsx index dfd9dbf6a2..9493d8b46b 100644 --- a/web/src/components/FileTypeIcon/index.tsx +++ b/web/src/components/FileTypeIcon/index.tsx @@ -144,7 +144,7 @@ export default function FileTypeIcon(props: { ); case FileType.folderOpen: return ( - + , + "children" + >, +) { + const { className, children, ...restProps } = props; + return ( - {props.children} + {children} ); } -function Item(props: { - children: React.ReactNode; - isActive: boolean; - className?: string; - key: string; - size?: "small" | "default"; - onClick?: () => void; -}) { - const { children, isActive, onClick, className, size = "default" } = props; +function Item( + props: { + children: React.ReactNode; + isActive: boolean; + className?: string; + size?: "small" | "default"; + } & Omit, "children">, +) { + const { children, isActive, className, size = "default", ...restProps } = props; + return (
  • {children}
  • 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 e49d23c9e6..9dbce84c5c 100644 --- a/web/src/pages/app/functions/mods/FunctionPanel/CreateModal/index.tsx +++ b/web/src/pages/app/functions/mods/FunctionPanel/CreateModal/index.tsx @@ -160,7 +160,7 @@ const CreateModal = (props: { pruneChildTree(child, data)); +} + +function pruneChildTree(node: TFunctionNode, data: TFunction[]): boolean { + if (!node.children || node.children.length === 0) { + return data.some((item) => item.name === node.name); + } + node.children = node.children.filter((child) => pruneChildTree(child, data)); + return node.children.length > 0 || data.some((item) => item.name === node.name); +} + +// get function dir not ended with / +function getFunctionDir(funcName: string, isFuncDir: "true" | "false" = "false") { + if (isFuncDir === "true" && funcName !== "" && !funcName.endsWith("/")) funcName += "/"; + if (isFuncDir === "false" && funcName.endsWith("/")) + funcName = funcName.slice(0, funcName.length - 1); + + const funcNameSplit = funcName.split("/"); + funcNameSplit.pop(); + return funcNameSplit.join("/"); +} + export default function FunctionList() { const { setCurrentFunction, @@ -77,64 +114,64 @@ export default function FunctionList() { const [currentTag, setCurrentTag] = useState(null); - const filterFunctions = useMemo(() => { - return allFunctionList.filter((item: any) => { - let flag = item?.name.includes(keywords); - - if (tagsList.length > 0 && currentTag) { - flag = flag && item.tags.includes(currentTag?.tagName); - } - - return flag; - }); - }, [allFunctionList, keywords, tagsList, currentTag]); - - function generateRoot(data: TFunctionNode[]) { - const root = functionRoot; - data.forEach((item) => { - const nameParts = item.name.split("/"); - let currentNode = root; - nameParts.forEach((part, index) => { - if (currentNode.children.find((node) => node.name === item.name)) { - const index = currentNode.children.findIndex((node) => node.name === item.name); - currentNode.children[index] = item; - return; - } else if (index === nameParts.length - 1) { - currentNode.children.push(item); - return; - } - let existingNode = currentNode.children.find( - (node) => node.name === part && node.level === index, - ); - if (!existingNode) { - const newNode = { - _id: item._id, - name: part, - level: index, - isExpanded: false, - children: [], - }; - currentNode.children.push(newNode); - existingNode = newNode; - } - currentNode = existingNode; + const generateRoot = useCallback( + (data: TFunction[]) => { + const root = cloneDeep(functionRoot); + data.forEach((item) => { + const nameParts = item.name.split("/"); + let currentNode = root; + nameParts.forEach((_, index) => { + if (currentNode.children.find((node) => node.name === item.name)) { + const index = currentNode.children.findIndex((node) => node.name === item.name); + currentNode.children[index] = item; + return; + } else if (index === nameParts.length - 1) { + currentNode.children.push(item); + return; + } + + const name = nameParts.slice(0, index + 1).join("/"); + let existingNode = currentNode.children.find( + (node) => node.name === name && node.level === index, + ); + if (!existingNode) { + // dir + const newNode = { + _id: item._id, + name, + level: index, + isExpanded: false, + children: [], + }; + currentNode.children.push(newNode); + existingNode = newNode; + } + currentNode = existingNode; + }); }); - }); - pruneTree(root, data); - return root; - } + pruneTree(root, data); + return root; + }, + [functionRoot], + ); - function pruneTree(node: TFunctionNode, data: TFunctionNode[]): void { - node.children = node.children.filter((child) => pruneChildTree(child, data)); - } + const filterFunctions = useMemo(() => { + const res = generateRoot( + allFunctionList.filter((item: any) => { + let flag = item?.name.includes(keywords); + if (tagsList.length > 0 && currentTag) { + flag = flag && item.tags.includes(currentTag?.tagName); + } + return flag; + }), + ); + return res.children; + }, [generateRoot, allFunctionList, keywords, tagsList, currentTag]); - function pruneChildTree(node: TFunctionNode, data: TFunctionNode[]): boolean { - if (!node.children || node.children.length === 0) { - return data.some((item) => item.name === node.name); - } - node.children = node.children.filter((child) => pruneChildTree(child, data)); - return node.children.length > 0 || data.some((item) => item.name === node.name); - } + const [draggedFunc, setDraggedFunc] = useState<{ + func: string; + isFuncDir: "true" | "false"; + } | null>(null); useFunctionListQuery({ onSuccess: (data) => { @@ -233,12 +270,12 @@ export default function FunctionList() { commonSettings.funcListDisplay === "name" || !item.desc ) { - return {nameParts[nameParts.length - 1]}; + return {nameParts[nameParts.length - 1]}; } else if (commonSettings.funcListDisplay === "desc") { - return {item.desc}; + return {item.desc}; } else if (commonSettings.funcListDisplay === "desc-name") { return ( - + {item.desc}
    {` ${ nameParts[nameParts.length - 1] @@ -247,7 +284,7 @@ export default function FunctionList() { ); } else { return ( - + {nameParts[nameParts.length - 1]}
    {` ${item.desc}`}
    @@ -255,37 +292,69 @@ export default function FunctionList() { } })(); + const onDragStart: DragEventHandler = (e) => { + const container = e.currentTarget?.parentElement; + if (!container) return; + const dataset = (e.target as any).dataset; + if (!dataset || !dataset.func) return; + + const el = document.createElement("div"); + el.textContent = dataset.func; + el.className = + "border border-green-500 rounded-lg py-1 px-2 font-medium tracking-wide max-w-fit"; + container.appendChild(el); + setDraggedFunc(dataset); + e.dataTransfer.setDragImage(el, -10, -10); + + setTimeout(() => { + container.removeChild(el); + }, 0); + }; + return ( { - if (!item?.children?.length) { + if (!item.children?.length) { setCurrentFunction(item); if (!recentFunctionList.map((item) => item._id).includes(item._id)) { setRecentFunctionList([item as unknown as TFunction, ...recentFunctionList]); } - navigate(`/app/${currentApp?.appid}/${Pages.function}/${item?.name}`); + navigate(`/app/${currentApp?.appid}/${Pages.function}/${item.name}`); } else { item.isExpanded = !item.isExpanded; setFunctionRoot({ ...functionRoot }); } }} + draggable + onDragStart={onDragStart} + data-func={item.name} + data-is-func-dir={!!item.children?.length} >
    - + {itemDisplay}
    @@ -334,8 +403,223 @@ export default function FunctionList() { }); } + const updateFunctionMutation = useUpdateFunctionMutation(); + const toast = useToast(); + const confirmDialog = useConfirmDialog(); + const [dragOverFuncDir, setDragOverFuncDir] = useState(null); + + const renameFunction = useCallback( + async (oldName: string, targetDirOrNewName: string, isNewName = false) => { + const func = allFunctionList.find((v) => v.name === oldName); + if (!func) return; + + let newName = targetDirOrNewName; + if (!isNewName) { + let targetDir = targetDirOrNewName; + if (targetDir !== "" && !targetDir.endsWith("/")) targetDir += "/"; + + const funcName = oldName.split("/").pop(); + newName = targetDir + funcName; + } + + const res = await updateFunctionMutation.mutateAsync({ + name: func.name, + description: func.desc, + websocket: func.websocket, + methods: func.methods, + code: func.source.code, + tags: func.tags, + newName, + }); + if (res.error) return; + + setRecentFunctionList( + recentFunctionList.map((item: any) => { + if (item.name === oldName) { + return { ...item, name: newName }; + } + return item; + }), + ); + if (currentFunction.name === oldName) { + setCurrentFunction({ ...currentFunction, name: newName }); + } + }, + [allFunctionList, currentFunction, recentFunctionList], + ); + + const renameFunctionDir = useCallback( + async (srcDir: string, targetDir: string) => { + if (srcDir !== "" && !srcDir.endsWith("/")) srcDir += "/"; + if (targetDir !== "" && !targetDir.endsWith("/")) targetDir += "/"; + + let dir = getFunctionDir(srcDir, "false"); + if (dir !== "") dir += "/"; + const funcs = allFunctionList.filter((func) => func.name.startsWith(srcDir)); + + return await Promise.allSettled( + funcs.map(async (v) => { + const newFuncName = targetDir + v.name.slice(dir.length); + return await renameFunction(v.name, newFuncName, true); + }), + ); + }, + [allFunctionList, renameFunction], + ); + + const onDrop: React.DragEventHandler = useCallback( + (e) => { + if (!draggedFunc) return; + const { func: srcFunc, isFuncDir: srcIsFuncDir } = draggedFunc; + + // find target position + let target = e.target as any; + while (target && !("func" in target.dataset)) { + if (target.id === "func-list") { + // out of search range + target = null; + break; + } + target = target.parentElement; + } + + const handleMoveToRootDir = () => { + // from root dir to root dir + if (!srcFunc.includes("/")) return; + + confirmDialog.show({ + headerText: String(t("MoveFunction")), + bodyText: String( + t("FunctionPanel.MoveFunctionToRootTip", { + srcFunc, + }), + ), + onConfirm: () => { + const toastId = toast({ + title: String(t("FunctionPanel.MovingFunction")), + status: "loading", + duration: null, + position: "top-left", + }); + const task = + srcIsFuncDir === "true" + ? renameFunctionDir(srcFunc, "") + : renameFunction(srcFunc, ""); + + task.finally(() => toast.close(toastId)); + }, + confirmButtonText: String(t("Confirm")), + }); + return; + }; + + // root dir + if (!target) { + return handleMoveToRootDir(); + } + + const { func: targetFunc, isFuncDir } = target.dataset; + // same file or file + if (targetFunc === srcFunc && isFuncDir === srcIsFuncDir) return; + + let targetDir = getFunctionDir(targetFunc, isFuncDir); + if (targetDir === "") { + return handleMoveToRootDir(); + } + + const srcFuncDir = getFunctionDir(srcFunc, srcIsFuncDir); + // not to root + // not to drag a dir to its inner dir + // not to drag a file to the same dir + if ( + targetDir && + !( + (srcIsFuncDir === "true" && targetDir.startsWith(srcFuncDir)) || + (srcIsFuncDir === "true" && targetDir === getFunctionDir(srcFunc, "false")) || + (srcIsFuncDir !== "true" && srcFuncDir === targetDir) + ) + ) { + targetDir += "/"; + confirmDialog.show({ + headerText: String(t("MoveFunction")), + bodyText: String( + t("FunctionPanel.MoveFunctionTip", { + srcFunc, + targetDir, + }), + ), + onConfirm: () => { + const toastId = toast({ + title: String(t("FunctionPanel.MovingFunction")), + status: "loading", + duration: null, + position: "top-left", + }); + const task = + srcIsFuncDir === "true" + ? renameFunctionDir(srcFunc, targetDir) + : renameFunction(srcFunc, targetDir); + + task.finally(() => toast.close(toastId)); + }, + confirmButtonText: String(t("Confirm")), + }); + } + }, + [renameFunction, renameFunctionDir, draggedFunc], + ); + + const onDragOver: React.DragEventHandler = useCallback( + (e) => { + if (!draggedFunc) return; + e.preventDefault(); + + // find target position + let target = e.target as any; + while (target && !("func" in target.dataset)) { + if (target.id === "func-list") { + // out of search range + target = null; + break; + } + target = target.parentElement; + } + + if (target) { + const { isFuncDir, func } = target.dataset; + let targetDir = getFunctionDir(func, isFuncDir); + const draggedFuncDir = getFunctionDir(draggedFunc.func, draggedFunc.isFuncDir); + // same file or dir + // drag a dir to its inner dir + // drag a file to the same dir + if ( + (func === draggedFunc.func && isFuncDir === draggedFunc.isFuncDir) || + (draggedFunc.isFuncDir === "true" && targetDir.startsWith(draggedFuncDir)) || + (draggedFunc.isFuncDir === "true" && + targetDir === getFunctionDir(draggedFunc.func, "false")) || + (draggedFunc.isFuncDir !== "true" && draggedFuncDir === targetDir) + ) { + setDragOverFuncDir(null); + return; + } + + setDragOverFuncDir(!targetDir ? "" : targetDir + "/"); + } else { + // from root to root + if (!draggedFunc.func.includes("/")) { + setDragOverFuncDir(null); + return; + } + // all files and dirs + setDragOverFuncDir(""); + } + }, + [draggedFunc], + ); + return ( + @@ -386,10 +670,18 @@ export default function FunctionList() { {renderSelectedTags()}
    - {keywords || currentTag ? ( - {renderSectionItems(filterFunctions, true)} - ) : functionRoot.children?.length ? ( - {renderSectionItems(functionRoot.children)} + {keywords || currentTag || functionRoot.children?.length ? ( + setDragOverFuncDir(null)} + onDragLeave={() => setDragOverFuncDir(null)} + id="func-list" + > + {keywords || currentTag + ? renderSectionItems(filterFunctions, true) + : renderSectionItems(functionRoot.children)} + ) : (

    {t("FunctionPanel.EmptyFunctionTip")}

    diff --git a/web/src/pages/app/functions/store.ts b/web/src/pages/app/functions/store.ts index 1b99d5d146..dd53c5f8f8 100644 --- a/web/src/pages/app/functions/store.ts +++ b/web/src/pages/app/functions/store.ts @@ -2,11 +2,11 @@ import { create } from "zustand"; import { devtools } from "zustand/middleware"; import { immer } from "zustand/middleware/immer"; -import { TFunction, TFunctionNode } from "@/apis/typing"; +import { TFunction } from "@/apis/typing"; import useGlobalStore from "@/pages/globalStore"; type State = { - allFunctionList: TFunctionNode[]; + allFunctionList: TFunction[]; recentFunctionList: TFunction[]; currentFunction: TFunction | { [key: string]: any }; currentRequestId: string | undefined; @@ -18,7 +18,7 @@ type State = { setCurrentRequestId: (requestId: string | undefined) => void; setCurrentFuncLogs: (logs: string) => void; setCurrentFuncTimeUsage: (timeUsage: string) => void; - setAllFunctionList: (functionList: TFunctionNode[]) => void; + setAllFunctionList: (functionList: TFunction[]) => void; setRecentFunctionList: (functionList: TFunction[]) => void; setCurrentFunction: (currentFunction: TFunction | { [key: string]: any }) => void; updateFunctionCode: (current: TFunction | { [key: string]: any }, codes: string) => void; diff --git a/web/src/pages/functionTemplate/CreateFuncTemplate/Mods/AddFunctionModal.tsx b/web/src/pages/functionTemplate/CreateFuncTemplate/Mods/AddFunctionModal.tsx index da3a69ff94..2e32072d15 100644 --- a/web/src/pages/functionTemplate/CreateFuncTemplate/Mods/AddFunctionModal.tsx +++ b/web/src/pages/functionTemplate/CreateFuncTemplate/Mods/AddFunctionModal.tsx @@ -134,7 +134,7 @@ export default async function (ctx: FunctionContext) { {...register("name", { required: true, pattern: { - value: /^[a-zA-Z0-9_.\-/]{1,256}$/, + value: /^[a-zA-Z0-9_.\-](?:[a-zA-Z0-9_.\-/]{0,254}[a-zA-Z0-9_.\-])?$/, message: t("FunctionPanel.FunctionNameRule"), }, })}