Skip to content

Commit 2f49a06

Browse files
authored
feat(web): add functions list folder (#1414)
* feat(web): add functions list folder * fix(web): fix recycle bin can not reopen
1 parent 7f16d4d commit 2f49a06

File tree

12 files changed

+170
-71
lines changed

12 files changed

+170
-71
lines changed

web/public/locales/en/translation.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
"Function": "Function",
9595
"FunctionList": "Functions",
9696
"FunctionName": "Function Name",
97-
"FunctionNameTip": "The unique identifier of the function, such as get-user",
97+
"FunctionNameTip": "The unique identifier of the function, such as get-user, can be used to create folders in the form of 'user/get-user'.",
9898
"InterfaceDebug": "Debug",
9999
"Methods": "Method",
100100
"Name": "KEY",
@@ -579,4 +579,4 @@
579579
"SelectOne": "At least select one",
580580
"Apply": "apply",
581581
"Developing": "Developing"
582-
}
582+
}

web/public/locales/zh-CN/translation.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
"Function": "函数",
9595
"FunctionList": "函数列表",
9696
"FunctionName": "函数名",
97-
"FunctionNameTip": "函数唯一标识, 如 get-user",
97+
"FunctionNameTip": "函数唯一标识如 get-user,可以使用 user/get-user 的形式来创建文件夹",
9898
"InterfaceDebug": "接口调试",
9999
"Methods": "请求方法",
100100
"Name": "参数名",
@@ -579,4 +579,4 @@
579579
"SelectOne": "请至少选择一个",
580580
"Apply": "使用",
581581
"Developing": "开发中"
582-
}
582+
}

web/public/locales/zh/translation.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
"Function": "函数",
9595
"FunctionList": "函数列表",
9696
"FunctionName": "函数名",
97-
"FunctionNameTip": "函数唯一标识, 如 get-user",
97+
"FunctionNameTip": "函数唯一标识如 get-user,可以使用 user/get-user 的形式来创建文件夹",
9898
"InterfaceDebug": "接口调试",
9999
"Methods": "请求方法",
100100
"Name": "参数名",

web/src/components/FileTypeIcon/index.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ export default function FileTypeIcon(props: {
3636
type: string;
3737
className?: string;
3838
fontSize?: number;
39+
width?: string;
3940
}) {
40-
const { type, className, fontSize } = props;
41+
const { type, className, fontSize, width = "18px" } = props;
4142

4243
switch (type) {
4344
case FileType.app:
@@ -141,7 +142,7 @@ export default function FileTypeIcon(props: {
141142
);
142143
case FileType.folder:
143144
return (
144-
<Icon viewBox="0 0 20 16" width="18px" className="align-middle">
145+
<Icon viewBox="0 0 20 16" width={width} className="align-middle">
145146
<path
146147
d="M2 16C1.45 16 0.979333 15.8043 0.588 15.413C0.196 15.021 0 14.55 0 14V2C0 1.45 0.196 0.979333 0.588 0.588C0.979333 0.196 1.45 0 2 0H8L10 2H18C18.55 2 19.021 2.196 19.413 2.588C19.8043 2.97933 20 3.45 20 4V14C20 14.55 19.8043 15.021 19.413 15.413C19.021 15.8043 18.55 16 18 16H2Z"
147148
fill="#47C8BF"

web/src/components/SectionList/index.module.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
li {
77
cursor: pointer;
88
display: flex;
9-
height: 24px;
9+
min-height: 24px;
1010
align-items: center;
1111
justify-content: space-between;
1212
padding: 0px 12px;

web/src/pages/app/database/PolicyDataList/index.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,9 @@ export default function PolicyDataList() {
9595
>
9696
<Button
9797
disabled={store.currentPolicy === undefined}
98-
colorScheme="primary"
99-
className="mr-2"
100-
style={{ width: "114px" }}
101-
leftIcon={<AddIcon />}
98+
leftIcon={<AddIcon fontSize={10} className="text-grayModern-500" />}
99+
variant="textGhost"
100+
size="xs"
102101
>
103102
{t("CollectionPanel.AddRules")}
104103
</Button>

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,13 @@ function EditorPanel() {
5959

6060
<HStack spacing={1}>
6161
<div className={clsx("flex items-center", !darkMode && "bg-[#F6F8F9]")}>
62-
<Input w={"200px"} size="xs" readOnly value={getFunctionUrl()} />
63-
<CopyText text={getFunctionUrl()} className="mr-3 !text-grayModern-300">
62+
<CopyText text={getFunctionUrl()}>
63+
<Input w={"200px"} size="xs" readOnly value={getFunctionUrl()} />
64+
</CopyText>
65+
<CopyText
66+
text={getFunctionUrl()}
67+
className="mr-3 cursor-pointer !text-grayModern-300"
68+
>
6469
<CopyIcon />
6570
</CopyText>
6671
</div>

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

+130-51
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* cloud functions list sidebar
33
***************************/
44

5-
import { useEffect, useState } from "react";
5+
import React, { useEffect, useState } from "react";
66
import { useNavigate, useParams } from "react-router-dom";
77
import { AddIcon, DeleteIcon, EditIcon, Search2Icon } from "@chakra-ui/icons";
88
import { Badge, HStack, Input, InputGroup, InputLeftElement, useColorMode } from "@chakra-ui/react";
@@ -24,7 +24,6 @@ import { useDeleteFunctionMutation, useFunctionListQuery } from "../../service";
2424
import useFunctionStore from "../../store";
2525
import TriggerModal from "../TriggerModal";
2626

27-
// import PromptModal from "./CreateModal/PromptModal";
2827
import CreateModal from "./CreateModal";
2928

3029
import { TFunction } from "@/apis/typing";
@@ -36,18 +35,27 @@ type TagItem = {
3635
selected: boolean;
3736
};
3837

38+
type TreeNode = {
39+
_id: string;
40+
name: string;
41+
level?: number;
42+
isExpanded?: boolean;
43+
children: (TreeNode | TFunction)[];
44+
};
45+
3946
export default function FunctionList() {
4047
const { setCurrentFunction, currentFunction, setAllFunctionList, allFunctionList } =
4148
useFunctionStore((store) => store);
4249

4350
const functionCache = useFunctionCache();
51+
const [root, setRoot] = useState<TreeNode>({ _id: "", name: "", children: [] });
4452

4553
const [keywords, setKeywords] = useState("");
4654

4755
const { colorMode } = useColorMode();
4856
const darkMode = colorMode === COLOR_MODE.dark;
4957

50-
const { currentApp } = useGlobalStore();
58+
const { currentApp, showSuccess } = useGlobalStore();
5159

5260
const { id: functionName } = useParams();
5361
const navigate = useNavigate();
@@ -63,9 +71,38 @@ export default function FunctionList() {
6371
return flag;
6472
});
6573

74+
function generateRoot(data: TFunction[]) {
75+
const root = { _id: "", name: "", level: 0, isExpanded: true, children: [] };
76+
data.forEach((item) => {
77+
const nameParts = item.name.split("/");
78+
let currentNode: TreeNode = root;
79+
nameParts.forEach((part, index) => {
80+
if (index === nameParts.length - 1) {
81+
currentNode.children.push(item);
82+
return;
83+
}
84+
let existingNode = currentNode.children.find((node) => node.name === part);
85+
if (!existingNode) {
86+
const newNode = {
87+
_id: item._id,
88+
name: part,
89+
level: index,
90+
isExpanded: false,
91+
children: [],
92+
};
93+
currentNode.children.push(newNode);
94+
existingNode = newNode;
95+
}
96+
currentNode = existingNode as TreeNode;
97+
});
98+
});
99+
return root;
100+
}
101+
66102
useFunctionListQuery({
67103
onSuccess: (data) => {
68104
setAllFunctionList(data.data);
105+
setRoot(generateRoot(data.data));
69106
const tags = data.data.reduce((pre: any, item: any) => {
70107
return pre.concat(item.tags);
71108
}, []);
@@ -125,6 +162,92 @@ export default function FunctionList() {
125162
) : null;
126163
};
127164

165+
function renderSectionItems(items: TreeNode[], isFuncList = false) {
166+
items.sort((a: TreeNode, b: TreeNode) => {
167+
const isFolderA = a.children && a.children.length > 0;
168+
const isFolderB = b.children && b.children.length > 0;
169+
if (isFolderA && !isFolderB) {
170+
return -1;
171+
} else if (!isFolderA && isFolderB) {
172+
return 1;
173+
}
174+
return 0;
175+
});
176+
177+
return items.map((item, index) => {
178+
let fileType = FileType.ts;
179+
if (item.children?.length) {
180+
fileType = FileType.folder;
181+
}
182+
const level = item.level || item?.name.split("/").length - 1;
183+
184+
return (
185+
<React.Fragment key={index}>
186+
<SectionList.Item
187+
isActive={item?.name === currentFunction?.name}
188+
key={index as any}
189+
className="group"
190+
onClick={() => {
191+
if (!item?.children?.length) {
192+
setCurrentFunction(item);
193+
navigate(`/app/${currentApp?.appid}/${Pages.function}/${item?.name}`);
194+
} else {
195+
item.isExpanded = !item.isExpanded;
196+
setRoot({ ...root });
197+
}
198+
}}
199+
>
200+
<div
201+
className={clsx(
202+
"overflow-hidden text-ellipsis whitespace-nowrap",
203+
!isFuncList ? `ml-${2 * level}` : "",
204+
)}
205+
>
206+
<FileTypeIcon type={fileType} width="12px" />
207+
<span className="ml-2 text-base">
208+
{item.children?.length || isFuncList ? item?.name : item?.name.split("/")[level]}
209+
</span>
210+
</div>
211+
{!item.children?.length && (
212+
<HStack spacing={1}>
213+
{functionCache.getCache(item?._id, (item as any)?.source?.code) !==
214+
(item as any)?.source?.code && (
215+
<span className="mt-[1px] inline-block h-1 w-1 flex-none rounded-full bg-warn-700"></span>
216+
)}
217+
<MoreButton isHidden={item.name !== currentFunction?.name} label={t("Operation")}>
218+
<>
219+
<CreateModal functionItem={item} tagList={tagsList}>
220+
<IconText icon={<EditIcon />} text={t("Edit")} />
221+
</CreateModal>
222+
<ConfirmButton
223+
onSuccessAction={async () => {
224+
const res = await deleteFunctionMutation.mutateAsync(item);
225+
if (!res.error) {
226+
showSuccess(t("DeleteSuccess"));
227+
}
228+
}}
229+
headerText={String(t("Delete"))}
230+
bodyText={String(t("FunctionPanel.DeleteConfirm"))}
231+
>
232+
<IconText
233+
icon={<DeleteIcon />}
234+
text={t("Delete")}
235+
className="hover:!text-error-600"
236+
/>
237+
</ConfirmButton>
238+
</>
239+
</MoreButton>
240+
</HStack>
241+
)}
242+
</SectionList.Item>
243+
{item.isExpanded &&
244+
item?.children?.length &&
245+
renderSectionItems(item.children as TreeNode[])}
246+
</React.Fragment>
247+
);
248+
});
249+
}
250+
128251
return (
129252
<Panel className="min-w-[215px] flex-grow overflow-hidden">
130253
<Panel.Header
@@ -172,56 +295,12 @@ export default function FunctionList() {
172295
{renderSelectedTags()}
173296

174297
<div className="flex-grow" style={{ overflowY: "auto" }}>
175-
{allFunctionList?.length ? (
298+
{keywords || currentTag ? (
176299
<SectionList>
177-
{filterFunctions.map((func: any) => {
178-
return (
179-
<SectionList.Item
180-
isActive={func?.name === currentFunction?.name}
181-
key={func?.name || ""}
182-
className="group"
183-
onClick={() => {
184-
setCurrentFunction(func);
185-
navigate(`/app/${currentApp?.appid}/${Pages.function}/${func?.name}`);
186-
}}
187-
>
188-
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
189-
<FileTypeIcon type={FileType.ts} />
190-
<span className="ml-2 text-base">{func?.name}</span>
191-
</div>
192-
<HStack spacing={1}>
193-
{functionCache.getCache(func?._id, func?.source?.code) !==
194-
func?.source?.code && (
195-
<span className="mt-[1px] inline-block h-1 w-1 flex-none rounded-full bg-warn-700"></span>
196-
)}
197-
<MoreButton
198-
isHidden={func.name !== currentFunction?.name}
199-
label={t("Operation")}
200-
>
201-
<>
202-
<CreateModal functionItem={func} tagList={tagsList}>
203-
<IconText icon={<EditIcon />} text={t("Edit")} />
204-
</CreateModal>
205-
<ConfirmButton
206-
onSuccessAction={async () => {
207-
await deleteFunctionMutation.mutateAsync(func);
208-
}}
209-
headerText={String(t("Delete"))}
210-
bodyText={String(t("FunctionPanel.DeleteConfirm"))}
211-
>
212-
<IconText
213-
icon={<DeleteIcon />}
214-
text={t("Delete")}
215-
className="hover:!text-error-600"
216-
/>
217-
</ConfirmButton>
218-
</>
219-
</MoreButton>
220-
</HStack>
221-
</SectionList.Item>
222-
);
223-
})}
300+
{renderSectionItems(filterFunctions as unknown as TreeNode[], true)}
224301
</SectionList>
302+
) : root.children?.length ? (
303+
<SectionList>{renderSectionItems(root.children as TreeNode[])}</SectionList>
225304
) : (
226305
<EmptyBox hideIcon>
227306
<p>{t("FunctionPanel.EmptyFunctionTip")}</p>

web/src/pages/app/logs/index.tsx

+13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState } from "react";
22
import { useForm } from "react-hook-form";
33
import { useTranslation } from "react-i18next";
4+
import { BiRefresh } from "react-icons/bi";
45
import {
56
Button,
67
Center,
@@ -120,6 +121,18 @@ export default function LogsPage() {
120121
>
121122
{t("Search")}
122123
</Button>
124+
125+
<Button
126+
size="xs"
127+
variant="textGhost"
128+
leftIcon={<BiRefresh fontSize={22} className="text-grayModern-500" />}
129+
disabled={logListQuery === undefined}
130+
onClick={() => {
131+
logListQuery.refetch();
132+
}}
133+
>
134+
{t("RefreshData")}
135+
</Button>
123136
</HStack>
124137
<Pagination
125138
options={LIMIT_OPTIONS}

web/src/pages/app/mods/StatusBar/RecycleBinModal/index.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export default function RecycleBinModal(props: { children: React.ReactElement })
165165
setFunctionListData(data.data);
166166
setCurrentFunction(data.data.list[0]);
167167
setEnable(false);
168-
if (data.data.list.length === 0) {
168+
if (data.data.total === 0) {
169169
onClose();
170170
showSuccess(t("RecycleBinEmpty"));
171171
}
@@ -177,7 +177,7 @@ export default function RecycleBinModal(props: { children: React.ReactElement })
177177
if (functionListData && functionListData?.list.length > 0) {
178178
onOpen();
179179
}
180-
}, [functionListData, onOpen]);
180+
}, [functionListData, onOpen, enable]);
181181

182182
return (
183183
<>
@@ -314,8 +314,10 @@ export default function RecycleBinModal(props: { children: React.ReactElement })
314314
<Pagination
315315
values={getPageInfo(functionListData)}
316316
onChange={(values: any) => {
317+
setEnable(true);
317318
setQueryData({
318-
...values,
319+
page: values.page,
320+
pageSize: values.pageSize,
319321
});
320322
}}
321323
notShowSelect

0 commit comments

Comments
 (0)