Commit 2ed1545e by Archer Committed by GitHub

V4.12.4 features (#5626)

* fix: push again, user select option button and form input radio content overflow (#5601)

* fix: push again, user select option button and form input radio content overflow

* fix: use useCallback instead of useMemo, fix unnecessary delete

* fix: Move the variable inside the component

* fix: do not pass valueLabel to MySelect

* ui

* del collection api adapt

* refactor: inherit permission (#5529)

* refactor: permission update conflict check function

* refactor(permission): app collaborator update api

* refactor(permission): support app update collaborator

* feat: support fe permission conflict check

* refactor(permission): app permission

* refactor(permission): dataset permission

* refactor(permission): team permission

* chore: fe adjust

* fix: type error

* fix: audit pagiation

* fix: tc

* chore: initv4130

* fix: app/dataset auth logic

* chore: move code

* refactor(permission): remove selfPermission

* fix: mock

* fix: test

* fix: app & dataset auth

* fix: inherit

* test(inheritPermission): test syncChildrenPermission

* prompt editor add list plugin (#5620)

* perf: search result (#5608)

* fix: table size (#5598)

* temp: list value

* backspace

* optimize code

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>

* fix: fe & member list (#5619)

* chore: initv4130

* fix: MemberItemCard

* fix: MemberItemCard

* chore: fe adjust & init script

* perf: test code

* doc

* fix debug variables (#5617)

* perf: search result (#5608)

* fix: table size (#5598)

* fix debug variables

* fix

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>

* perf: member ui

* fix: inherit bug (#5624)

* refactor(permission): remove getClbsWithInfo, which is useless

* fix: app list privateApp

* fix: get infos

* perf(fe): remove delete icon when it is disable in MemberItemCard

* fix: dataset private dataset

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* perf: auto coupon

* chore: upgrade script & get infos avatar  (#5625)

* fix: get infos

* chore: initv4130

* feat: support WecomRobot publish, and fix AesKey can not save bug (#5526)

* feat: resolve conflicts

* fix: add param 'show_publish_wecom'

* feat: abstract out WecomCrypto type

* doc: wecom robot document

* fix: solve instability in AI output

* doc: update some pictures

* feat: remove functions from request.ts to chat.ts and toolCall.ts

* doc: wecom robot doc update

* fix

* delete unused code

* doc: update version and prompt

* feat: remove wecom crypto, delete wecom code in workflow

* feat: delete unused codes

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* remove test

* rename init shell

* feat: collection page store

* reload sandbox

* pysandbox

* remove log

* chore: remove useless code (#5629)

* chore: remove useless code

* fix: checkConflict

* perf: support hidden type for RoleList

* fix: copy node

* update doc

* fix(permission): some bug (#5632)

* fix: app/dataset list

* fix: inherit bug

* perf: del app;i18n;save chat

* fix: test

* i18n

* fix: sumper overflow return OwnerRoleVal (#5633)

* remove invalid code

* fix: scroll

* fix: objectId

* update next

* update package

* object id

* mock redis

* feat: add redis append to resolve wecom stream response  (#5643)

* feat: resolve conflicts

* fix: add param 'show_publish_wecom'

* feat: abstract out WecomCrypto type

* doc: wecom robot document

* fix: solve instability in AI output

* doc: update some pictures

* feat: remove functions from request.ts to chat.ts and toolCall.ts

* doc: wecom robot doc update

* fix

* delete unused code

* doc: update version and prompt

* feat: remove wecom crypto, delete wecom code in workflow

* feat: delete unused codes

* feat: add redis append method

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* cache per

* fix(test): init team sub when creating mocked user (#5646)

* fix: button is not vertically centered (#5647)

* doc

* fix: gridFs objectId (#5649)

---------

Co-authored-by: Zeng Qingwen <143274079+fishwww-ww@users.noreply.github.com>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: heheer <zhiyu44@qq.com>
parent c8934e3d
...@@ -868,8 +868,12 @@ curl --location --request PUT 'http://localhost:3000/api/core/dataset/collection ...@@ -868,8 +868,12 @@ curl --location --request PUT 'http://localhost:3000/api/core/dataset/collection
<Tab value="请求示例" > <Tab value="请求示例" >
```bash ```bash
curl --location --request DELETE 'http://localhost:3000/api/core/dataset/collection/delete?id=65aa2a64e6cb9b8ccdc00de8' \ curl --location --request POST 'http://localhost:3000/api/core/dataset/collection/delete' \
--header 'Authorization: Bearer {{authorization}}' \ --header 'Authorization: Bearer fastgpt-' \
--header 'Content-Type: application/json' \
--data-raw '{
"collectionIds": ["65a8cdcb0d70d3de0bf08d0a"]
}'
``` ```
</Tab> </Tab>
...@@ -877,7 +881,7 @@ curl --location --request DELETE 'http://localhost:3000/api/core/dataset/collect ...@@ -877,7 +881,7 @@ curl --location --request DELETE 'http://localhost:3000/api/core/dataset/collect
<Tab value="参数说明" > <Tab value="参数说明" >
<div> <div>
- id: 集合的ID - collectionIds: 集合的 ID 列表
</div> </div>
</Tab> </Tab>
......
...@@ -102,6 +102,7 @@ description: FastGPT 文档目录 ...@@ -102,6 +102,7 @@ description: FastGPT 文档目录
- [/docs/upgrading/4-12/4121](/docs/upgrading/4-12/4121) - [/docs/upgrading/4-12/4121](/docs/upgrading/4-12/4121)
- [/docs/upgrading/4-12/4122](/docs/upgrading/4-12/4122) - [/docs/upgrading/4-12/4122](/docs/upgrading/4-12/4122)
- [/docs/upgrading/4-12/4123](/docs/upgrading/4-12/4123) - [/docs/upgrading/4-12/4123](/docs/upgrading/4-12/4123)
- [/docs/upgrading/4-12/4124](/docs/upgrading/4-12/4124)
- [/docs/upgrading/4-8/40](/docs/upgrading/4-8/40) - [/docs/upgrading/4-8/40](/docs/upgrading/4-8/40)
- [/docs/upgrading/4-8/41](/docs/upgrading/4-8/41) - [/docs/upgrading/4-8/41](/docs/upgrading/4-8/41)
- [/docs/upgrading/4-8/42](/docs/upgrading/4-8/42) - [/docs/upgrading/4-8/42](/docs/upgrading/4-8/42)
...@@ -180,3 +181,4 @@ description: FastGPT 文档目录 ...@@ -180,3 +181,4 @@ description: FastGPT 文档目录
- [/docs/use-cases/external-integration/feishu](/docs/use-cases/external-integration/feishu) - [/docs/use-cases/external-integration/feishu](/docs/use-cases/external-integration/feishu)
- [/docs/use-cases/external-integration/official_account](/docs/use-cases/external-integration/official_account) - [/docs/use-cases/external-integration/official_account](/docs/use-cases/external-integration/official_account)
- [/docs/use-cases/external-integration/openapi](/docs/use-cases/external-integration/openapi) - [/docs/use-cases/external-integration/openapi](/docs/use-cases/external-integration/openapi)
- [/docs/use-cases/external-integration/wecom](/docs/use-cases/external-integration/wecom)
---
title: 'V4.12.4(进行)'
description: 'FastGPT V4.12.4 更新说明'
---
## 🚀 新增内容
1. 商业版支持企微发布渠道。
## ⚙️ 优化
1. 权限继承优化,子资源权限高于父级时,不会强制打断继承模式。
2. Prompt 编辑器支持列表渲染。
3. 数据页返回知识库列表,保持分页。
4. 知识库上传文件成功后,返回对应上传目录。
5. 删除应用,减少事务操作。
6. 用户选择 UI。
## 🐛 修复
1. HTTP 工具空指针,导致无法编辑。
2. python 代码运行,入参无法是 boolean 值。
3. debug 模式下,全局变量未传递。
## 🔨 插件更新
{ {
"title": "4.12.x", "title": "4.12.x",
"description": "", "description": "",
"pages": ["4123", "4122", "4121", "4120"] "pages": ["4124", "4123", "4122", "4121", "4120"]
} }
{ {
"title": "外部调用 FastGPT", "title": "外部调用 FastGPT",
"description": "外部应用通过多种方式调用 FastGPT 功能的教程", "description": "外部应用通过多种方式调用 FastGPT 功能的教程",
"pages": ["openapi", "feishu", "dingtalk", "official_account"] "pages": ["openapi", "feishu", "dingtalk", "wecom", "official_account"]
} }
---
title: 接入企微机器人教程
description: FastGPT 接入企微机器人教程
---
从 4.12.4 版本起,FastGPT 商业版支持直接接入企微机器人,无需额外的 API。
## 1.配置可信域名和可信IP
点击企微头像,打开管理企业
![图片](/imgs/wecom-bot-1.png)
在应用管理中找到"自建"-"创建应用"
![图片](/imgs/wecom-bot-2.png)
创建好应用后, 下拉, 依次配置"网页授权及JS-SDK"和"企业可信IP"
![图片](/imgs/wecom-bot-3.png)
其中, 网页授权及JS-SDK要求按照企微指引,完成域名归属认证
![图片](/imgs/wecom-bot-4.png)
企业可信IP要求为企业服务器IP, 后续企微的回调URL将请求到此IP
![图片](/imgs/wecom-bot-5.png)
## 2. 创建企业自建应用
前往 FastGPT ,选择想要接入的应用,在 发布渠道 页面,新建一个接入企微智能机器人的发布渠道,填写好基础信息。
![图片](/imgs/wecom-bot-6.png)
现在回到企业微信平台,找到 Corp ID, Agent ID, Token, AES Key 信息并填写回 FastGPT 平台
![图片](/imgs/wecom-bot-7.png)
在"我的企业"里找到企业 ID, 填写到 FastGPT 的 Corp ID 中
![图片](/imgs/wecom-bot-8.png)
在应用中找到 Agent Id 和 Secret, 并填写回 FastGPT
![图片](/imgs/wecom-bot-9.png)
点击"消息接收"-"设置API接收"
![图片](/imgs/wecom-bot-10.png)
随机生成或者手动输入 Token 和 Encoding-Key, 分别填写到 FastGPT 的 Token 和 AES Key 中
![图片](/imgs/wecom-bot-11.png)
填写完成后确认创建
然后点击请求地址, 复制页面中的链接
![图片](/imgs/wecom-bot-12.png)
回到刚才的配置详情, 将刚才复制的链接填入 URL 框中, 并点击创建
注意: 若复制的链接是以 "http://localhost" 开头, 需要将本地地址改为企业主体域名
因为企微会给填写的 URL 发送验证密文, 若 URL 为本地地址, 则本地接收不到企微的密文
若 URL 不是企业主体域名, 则验证会失败
## 3. 创建智能机器人
在"安全与管理" - "管理工具"页面找到"智能机器人" ( 注意: 只有企业创建者或超级管理员才有权限看到这个入口 )
![图片](/imgs/wecom-bot-13.png)
创建机器人页面,下拉,找到,点击"API模式创建"
![图片](/imgs/wecom-bot-14.png)
与刚才配置自建应用同理, 配置这三个参数
![图片](/imgs/wecom-bot-15.png)
注意: 这里的 Agent ID , 和上面的不同, 可以先随意填写一个值, 后续会根据企业微信提供的数据重新更改
Secret 为用户自己决定的密令
填写完成后确认创建
然后点击请求地址, 复制页面中的链接, 链接的地址也必须为企业主体域名
创建完成后, 找到智能机器人的配置详情
![图片](/imgs/wecom-bot-16.png)
复制 Bot ID, 填写到 FastGPT 的 Agent ID 中
![图片](/imgs/wecom-bot-17.png)
## 4. 使用智能机器人
在企业微信平台的"通讯录",即可找到创建的机器人,就可以发送消息了
![图片](/imgs/wecom-bot-18.png)
## FAQ
### 发送了消息,没响应
1. 检查企微机器人回调地址、权限等是否正确。
2. 查看 FastGPT 对话日志,是否有对应的提问记录
3. 如果没记录,则可能是应用运行报错了,可以先试试最简单的机器人。(飞书机器人无法输入全局变量、文件、图片内容)
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
"document/content/docs/introduction/development/modelConfig/ppio.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/modelConfig/ppio.mdx": "2025-08-05T23:20:39+08:00",
"document/content/docs/introduction/development/modelConfig/siliconCloud.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/modelConfig/siliconCloud.mdx": "2025-08-05T23:20:39+08:00",
"document/content/docs/introduction/development/openapi/chat.mdx": "2025-08-14T18:54:47+08:00", "document/content/docs/introduction/development/openapi/chat.mdx": "2025-08-14T18:54:47+08:00",
"document/content/docs/introduction/development/openapi/dataset.mdx": "2025-08-14T18:54:47+08:00", "document/content/docs/introduction/development/openapi/dataset.mdx": "2025-09-11T10:29:11+08:00",
"document/content/docs/introduction/development/openapi/intro.mdx": "2025-08-14T18:54:47+08:00", "document/content/docs/introduction/development/openapi/intro.mdx": "2025-08-14T18:54:47+08:00",
"document/content/docs/introduction/development/openapi/share.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/openapi/share.mdx": "2025-08-05T23:20:39+08:00",
"document/content/docs/introduction/development/proxy/cloudflare.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/development/proxy/cloudflare.mdx": "2025-07-23T21:35:03+08:00",
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
"document/content/docs/introduction/development/sealos.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/sealos.mdx": "2025-08-05T23:20:39+08:00",
"document/content/docs/introduction/guide/DialogBoxes/htmlRendering.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/DialogBoxes/htmlRendering.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/DialogBoxes/quoteList.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/DialogBoxes/quoteList.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/admin/sso.mdx": "2025-07-24T13:00:27+08:00", "document/content/docs/introduction/guide/admin/sso.mdx": "2025-09-08T20:07:04+08:00",
"document/content/docs/introduction/guide/admin/teamMode.mdx": "2025-08-27T16:59:57+08:00", "document/content/docs/introduction/guide/admin/teamMode.mdx": "2025-08-27T16:59:57+08:00",
"document/content/docs/introduction/guide/course/ai_settings.mdx": "2025-07-24T13:00:27+08:00", "document/content/docs/introduction/guide/course/ai_settings.mdx": "2025-07-24T13:00:27+08:00",
"document/content/docs/introduction/guide/course/chat_input_guide.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/course/chat_input_guide.mdx": "2025-07-23T21:35:03+08:00",
...@@ -97,15 +97,16 @@ ...@@ -97,15 +97,16 @@
"document/content/docs/protocol/terms.en.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/protocol/terms.en.mdx": "2025-08-03T22:37:45+08:00",
"document/content/docs/protocol/terms.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/protocol/terms.mdx": "2025-08-03T22:37:45+08:00",
"document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00", "document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00",
"document/content/docs/toc.mdx": "2025-08-29T01:24:19+08:00", "document/content/docs/toc.mdx": "2025-09-12T12:58:39+08:00",
"document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-10/4101.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-10/4101.mdx": "2025-09-08T20:07:20+08:00",
"document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00",
"document/content/docs/upgrading/4-11/4111.mdx": "2025-08-07T22:49:09+08:00", "document/content/docs/upgrading/4-11/4111.mdx": "2025-08-07T22:49:09+08:00",
"document/content/docs/upgrading/4-12/4120.mdx": "2025-09-07T14:41:48+08:00", "document/content/docs/upgrading/4-12/4120.mdx": "2025-09-07T14:41:48+08:00",
"document/content/docs/upgrading/4-12/4121.mdx": "2025-09-07T14:41:48+08:00", "document/content/docs/upgrading/4-12/4121.mdx": "2025-09-07T14:41:48+08:00",
"document/content/docs/upgrading/4-12/4122.mdx": "2025-09-07T14:41:48+08:00", "document/content/docs/upgrading/4-12/4122.mdx": "2025-09-07T14:41:48+08:00",
"document/content/docs/upgrading/4-12/4123.mdx": "2025-09-07T14:41:48+08:00", "document/content/docs/upgrading/4-12/4123.mdx": "2025-09-07T20:55:14+08:00",
"document/content/docs/upgrading/4-12/4124.mdx": "2025-09-13T01:34:04+08:00",
"document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",
...@@ -185,5 +186,6 @@ ...@@ -185,5 +186,6 @@
"document/content/docs/use-cases/external-integration/feishu.mdx": "2025-07-24T14:23:04+08:00", "document/content/docs/use-cases/external-integration/feishu.mdx": "2025-07-24T14:23:04+08:00",
"document/content/docs/use-cases/external-integration/official_account.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/use-cases/external-integration/official_account.mdx": "2025-08-05T23:20:39+08:00",
"document/content/docs/use-cases/external-integration/openapi.mdx": "2025-08-14T18:54:47+08:00", "document/content/docs/use-cases/external-integration/openapi.mdx": "2025-08-14T18:54:47+08:00",
"document/content/docs/use-cases/external-integration/wecom.mdx": "2025-09-12T12:58:39+08:00",
"document/content/docs/use-cases/index.mdx": "2025-07-24T14:23:04+08:00" "document/content/docs/use-cases/index.mdx": "2025-07-24T14:23:04+08:00"
} }
\ No newline at end of file
...@@ -20,7 +20,6 @@ ...@@ -20,7 +20,6 @@
}, },
"devDependencies": { "devDependencies": {
"@chakra-ui/cli": "^2.4.1", "@chakra-ui/cli": "^2.4.1",
"typescript": "^5.1.3",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
"@vitest/coverage-v8": "^3.0.9", "@vitest/coverage-v8": "^3.0.9",
...@@ -28,13 +27,14 @@ ...@@ -28,13 +27,14 @@
"eslint-config-next": "^14.1.0", "eslint-config-next": "^14.1.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"i18next": "23.16.8", "i18next": "23.16.8",
"js-yaml": "^4.1.0",
"lint-staged": "^13.3.0", "lint-staged": "^13.3.0",
"mongodb-memory-server": "^10.1.4",
"next-i18next": "15.4.2", "next-i18next": "15.4.2",
"prettier": "3.2.4", "prettier": "3.2.4",
"react-i18next": "14.1.2", "react-i18next": "14.1.2",
"typescript": "^5.1.3",
"vitest": "^3.0.9", "vitest": "^3.0.9",
"js-yaml": "^4.1.0",
"mongodb-memory-server": "^10.1.4",
"zhlint": "^0.7.4" "zhlint": "^0.7.4"
}, },
"lint-staged": { "lint-staged": {
......
...@@ -70,6 +70,7 @@ export type FastGPTFeConfigsType = { ...@@ -70,6 +70,7 @@ export type FastGPTFeConfigsType = {
show_dataset_yuque?: boolean; show_dataset_yuque?: boolean;
show_publish_feishu?: boolean; show_publish_feishu?: boolean;
show_publish_dingtalk?: boolean; show_publish_dingtalk?: boolean;
show_publish_wecom?: boolean;
show_publish_offiaccount?: boolean; show_publish_offiaccount?: boolean;
show_dataset_enhance?: boolean; show_dataset_enhance?: boolean;
......
...@@ -109,7 +109,7 @@ export const valueTypeFormat = (value: any, valueType?: WorkflowIOValueTypeEnum) ...@@ -109,7 +109,7 @@ export const valueTypeFormat = (value: any, valueType?: WorkflowIOValueTypeEnum)
return typeof value === 'object' ? JSON.stringify(value) : String(value); return typeof value === 'object' ? JSON.stringify(value) : String(value);
} }
if (valueType === WorkflowIOValueTypeEnum.number) { if (valueType === WorkflowIOValueTypeEnum.number) {
if (value === '') return undefined; if (value === '') return null;
return Number(value); return Number(value);
} }
if (valueType === WorkflowIOValueTypeEnum.boolean) { if (valueType === WorkflowIOValueTypeEnum.boolean) {
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
"@fastgpt-sdk/plugin": "^0.1.16", "@fastgpt-sdk/plugin": "^0.1.16",
"@apidevtools/swagger-parser": "^10.1.0", "@apidevtools/swagger-parser": "^10.1.0",
"@bany/curl-to-json": "^1.2.8", "@bany/curl-to-json": "^1.2.8",
"axios": "^1.8.2", "axios": "^1.12.1",
"cron-parser": "^4.9.0", "cron-parser": "^4.9.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"encoding": "^0.1.13", "encoding": "^0.1.13",
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
"jschardet": "3.1.1", "jschardet": "3.1.1",
"json5": "^2.2.3", "json5": "^2.2.3",
"nanoid": "^5.1.3", "nanoid": "^5.1.3",
"next": "14.2.28", "next": "14.2.32",
"openai": "4.61.0", "openai": "4.61.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"timezones-list": "^3.0.2", "timezones-list": "^3.0.2",
......
import type { UpdateAppCollaboratorBody } from 'core/app/collaborator';
import type { RequireOnlyOne } from '../../common/type/utils'; import type { RequireOnlyOne } from '../../common/type/utils';
import { RequireAtLeastOne } from '../../common/type/utils'; import { RequireAtLeastOne } from '../../common/type/utils';
import type { Permission } from './controller'; import type { Permission } from './controller';
import type { PermissionValueType } from './type'; import type { PermissionValueType, RoleValueType } from './type';
export type CollaboratorItemType = { export type CollaboratorIdType = RequireOnlyOne<{
teamId: string;
permission: Permission;
name: string;
avatar: string;
} & RequireOnlyOne<{
tmbId: string; tmbId: string;
groupId: string; groupId: string;
orgId: string; orgId: string;
}>; }>;
export type UpdateClbPermissionProps<addOnly = false> = { export type CollaboratorItemDetailType = {
members?: string[]; teamId: string;
groups?: string[]; permission: Permission;
orgs?: string[]; name: string;
} & (addOnly extends true avatar: string;
? {} } & CollaboratorIdType;
: {
permission: PermissionValueType; export type CollaboratorItemType = {
}); permission: PermissionValueType;
} & CollaboratorIdType;
export type DeletePermissionQuery = RequireOnlyOne<{ export type UpdateClbPermissionProps = {
tmbId?: string; collaborators: CollaboratorItemType[];
groupId?: string; };
orgId?: string;
}>; export type DeletePermissionQuery = CollaboratorIdType;
export type CollaboratorListType = {
clbs: CollaboratorItemDetailType[];
parentClbs?: CollaboratorItemDetailType[];
};
import type { UserModelSchema } from '../user/type'; import type { UserModelSchema } from '../user/type';
import type { RequireOnlyOne } from '../../common/type/utils'; import type { RequireOnlyOne } from '../../common/type/utils';
import type { TeamMemberSchema } from '../user/team/type'; import type { TeamMemberSchema } from '../user/team/type';
import { MemberGroupSchemaType } from './memberGroup/type'; import type { CommonRoleKeyEnum } from './constant';
import type { TeamMemberWithUserSchema } from '../user/team/type'; import { type CommonPerKeyEnum, type PerResourceTypeEnum } from './constant';
import type { CommonPerKeyEnum, CommonRoleKeyEnum } from './constant';
import { AuthUserTypeEnum, type CommonPerKeyEnum, type PerResourceTypeEnum } from './constant';
// PermissionValueType, the type of permission's value is a number, which is a bit field actually. // PermissionValueType, the type of permission's value is a number, which is a bit field actually.
// It is spired by the permission system in Linux. // It is spired by the permission system in Linux.
...@@ -18,14 +16,14 @@ export type ResourceType = `${PerResourceTypeEnum}`; ...@@ -18,14 +16,14 @@ export type ResourceType = `${PerResourceTypeEnum}`;
/** /**
* Define the roles. Each role is a binary number, only one bit is set to 1. * Define the roles. Each role is a binary number, only one bit is set to 1.
*/ */
export type RoleListType<T = {}> = Readonly< export type RoleListType<T extends string | number | symbol = CommonRoleKeyEnum> = Readonly<
Record< Record<
T | CommonRoleKeyEnum, T | CommonRoleKeyEnum,
Readonly<{ Readonly<{
name: string; name: string;
description: string; description: string;
value: RoleValueType; value: RoleValueType;
checkBoxType: 'single' | 'multiple'; checkBoxType: 'single' | 'multiple' | 'hidden';
}> }>
> >
>; >;
...@@ -43,7 +41,7 @@ export type RoleListType<T = {}> = Readonly< ...@@ -43,7 +41,7 @@ export type RoleListType<T = {}> = Readonly<
* write: 0b110, // bad, should be 0b010 * write: 0b110, // bad, should be 0b010
* } * }
*/ */
export type PermissionListType<T = {}> = Readonly< export type PermissionListType<T extends string | number | symbol = CommonPerKeyEnum> = Readonly<
Record<T | CommonPerKeyEnum, PermissionValueType> Record<T | CommonPerKeyEnum, PermissionValueType>
>; >;
......
...@@ -31,11 +31,13 @@ export const TeamPerList: PermissionListType<TeamPerKeyEnum> = { ...@@ -31,11 +31,13 @@ export const TeamPerList: PermissionListType<TeamPerKeyEnum> = {
export const TeamRoleList: RoleListType<TeamRoleKeyEnum> = { export const TeamRoleList: RoleListType<TeamRoleKeyEnum> = {
[CommonPerKeyEnum.read]: { [CommonPerKeyEnum.read]: {
...CommonRoleList[CommonPerKeyEnum.read], ...CommonRoleList[CommonPerKeyEnum.read],
name: i18nT('common:permission.common_member'),
value: 0b000100 value: 0b000100
}, },
[CommonPerKeyEnum.write]: { [CommonPerKeyEnum.write]: {
...CommonRoleList[CommonPerKeyEnum.write], ...CommonRoleList[CommonPerKeyEnum.write],
value: 0b000010 value: 0b000010,
checkBoxType: 'hidden'
}, },
[CommonPerKeyEnum.manage]: { [CommonPerKeyEnum.manage]: {
...CommonRoleList[CommonPerKeyEnum.manage], ...CommonRoleList[CommonPerKeyEnum.manage],
......
import type { CollaboratorIdType, CollaboratorItemType } from './collaborator';
import { ManageRoleVal, OwnerRoleVal } from './constant';
import type { RoleValueType } from './type';
import { type PermissionValueType } from './type'; import { type PermissionValueType } from './type';
import { NullRoleVal, PermissionTypeEnum } from './constant'; /**
import type { Permission } from './controller'; * Sum the permission value.
* If no permission value is provided, return undefined to fallback to default value.
/* team public source, or owner source in team */ * @param per permission value (number)
export function mongoRPermission({ * @returns sum of permission value
teamId, */
tmbId,
permission
}: {
teamId: string;
tmbId: string;
permission: Permission;
}) {
if (permission.isOwner) {
return {
teamId
};
}
return {
teamId,
$or: [{ permission: PermissionTypeEnum.public }, { tmbId }]
};
}
export function mongoOwnerPermission({ teamId, tmbId }: { teamId: string; tmbId: string }) {
return {
teamId,
tmbId
};
}
// return permission-related schema to define the schema of resources
export function getPermissionSchema(defaultPermission: PermissionValueType = NullRoleVal) {
return {
defaultPermission: {
type: Number,
default: defaultPermission
},
inheritPermission: {
type: Boolean,
default: true
}
};
}
export const sumPer = (...per: PermissionValueType[]) => { export const sumPer = (...per: PermissionValueType[]) => {
if (per.length === 0) { if (per.length === 0) {
// prevent sum 0 value, to fallback to default value // prevent sum 0 value, to fallback to default value
return undefined; return undefined;
} }
return per.reduce((acc, cur) => acc | cur, 0); const res = per.reduce((acc, cur) => acc | cur, 0);
if (res < 0) {
// overflowed
return OwnerRoleVal;
}
return res;
};
/**
* Check if the update cause conflict (need to remove inheritance permission).
* Conflict condition:
* The updated collaborator is a parent collaborator.
* @param parentClbs parent collaborators
* @param oldChildClbs old child collaborators
* @param newChildClbs new child collaborators
*/
export const checkRoleUpdateConflict = ({
parentClbs,
newChildClbs
}: {
parentClbs: CollaboratorItemType[];
newChildClbs: CollaboratorItemType[];
}): boolean => {
if (parentClbs.length === 0) {
return false;
}
// Use a Map for faster lookup by teamId
const parentClbRoleMap = new Map(
parentClbs.map((clb) => [
getCollaboratorId(clb),
{
...clb
}
])
);
const changedClbs = getChangedCollaborators({
newRealClbs: newChildClbs,
oldRealClbs: parentClbs
});
for (const changedClb of changedClbs) {
const parent = parentClbRoleMap.get(getCollaboratorId(changedClb));
if (parent && ((changedClb.changedRole & parent.permission) !== 0 || changedClb.deleted)) {
return true;
}
}
return false;
};
export type ChangedClbType = {
changedRole: RoleValueType;
deleted: boolean;
} & CollaboratorIdType;
/**
* Get changed collaborators.
* return empty array if all collaborators are unchanged.
*
* for each return item:
* ```typescript
* {
* // ... ids
* changedRole: number; // set bit means the role is changed
* deleted: boolean; // is deleted
* }
* ```
*
* **special**: for low 3 bit: always get the lowest change, unset the higher change.
*/
export const getChangedCollaborators = ({
oldRealClbs,
newRealClbs
}: {
oldRealClbs: CollaboratorItemType[];
newRealClbs: CollaboratorItemType[];
}): ChangedClbType[] => {
if (oldRealClbs.length === 0) {
return newRealClbs.map((clb) => ({
...clb,
changedRole: clb.permission,
deleted: false
}));
}
const oldClbsMap = new Map(oldRealClbs.map((clb) => [getCollaboratorId(clb), clb]));
const changedClbs: ChangedClbType[] = [];
for (const newClb of newRealClbs) {
const oldClb = oldClbsMap.get(getCollaboratorId(newClb));
if (!oldClb) {
changedClbs.push({
...newClb,
changedRole: newClb.permission,
deleted: false
});
continue;
}
const changedRole = oldClb.permission ^ newClb.permission;
if (changedRole) {
changedClbs.push({
...newClb,
changedRole,
deleted: false
});
}
}
for (const oldClb of oldRealClbs) {
const newClb = newRealClbs.find((clb) => getCollaboratorId(clb) === getCollaboratorId(oldClb));
if (!newClb) {
changedClbs.push({
...oldClb,
changedRole: oldClb.permission,
deleted: true
});
}
}
changedClbs.forEach((clb) => {
// For the lowest 3 bits, only keep the lowest set bit as 1, clear other lower bits, keep higher bits unchanged
const low3 = clb.changedRole & 0b111;
const lowestBit = low3 & -low3;
clb.changedRole = (clb.changedRole & ~0b111) | lowestBit;
});
return changedClbs;
};
export const getCollaboratorId = (clb: CollaboratorIdType) =>
(clb.tmbId || clb.groupId || clb.orgId)!;
export const mergeCollaboratorList = <T extends CollaboratorItemType>({
parentClbs,
childClbs
}: {
parentClbs: T[];
childClbs: T[];
}) => {
const idToClb = new Map<string, T>();
// Add all items from list1
for (const parentClb of parentClbs) {
if (parentClb.permission === OwnerRoleVal) {
idToClb.set(getCollaboratorId(parentClb), { ...parentClb, permission: ManageRoleVal });
continue;
}
idToClb.set(getCollaboratorId(parentClb), { ...parentClb });
}
// Merge permissions from list2
for (const childClb of childClbs) {
const id = getCollaboratorId(childClb);
if (idToClb.has(id)) {
// If already exists, merge permission bits
const original = idToClb.get(id)!;
idToClb.set(id, {
...original,
permission: sumPer(original.permission, childClb.permission)!
});
} else {
idToClb.set(id, { ...childClb });
}
}
return Array.from(idToClb.values());
}; };
import type { StandardSubLevelEnum, SubModeEnum } from '../sub/constants'; import type { StandardSubLevelEnum, SubModeEnum } from '../sub/constants';
import type { BillTypeEnum } from './constants'; import type { BillTypeEnum, BillPayWayEnum } from './constants';
import { DrawBillQRItem } from './constants'; import { DrawBillQRItem } from './constants';
export type CreateOrderResponse = { export type CreateOrderResponse = {
......
import { retryFn } from '@fastgpt/global/common/system/utils'; import { retryFn } from '@fastgpt/global/common/system/utils';
import { connectionMongo } from '../../mongo'; import { connectionMongo, Types } from '../../mongo';
import { MongoRawTextBufferSchema, bucketName } from './schema'; import { MongoRawTextBufferSchema, bucketName } from './schema';
import { addLog } from '../../system/log'; import { addLog } from '../../system/log';
import { setCron } from '../../system/cron'; import { setCron } from '../../system/cron';
...@@ -86,7 +86,7 @@ export const getRawTextBuffer = async (sourceId: string) => { ...@@ -86,7 +86,7 @@ export const getRawTextBuffer = async (sourceId: string) => {
} }
// Read file content // Read file content
const downloadStream = gridBucket.openDownloadStream(bufferData._id); const downloadStream = gridBucket.openDownloadStream(new Types.ObjectId(bufferData._id));
const fileBuffers = await gridFsStream2Buffer(downloadStream); const fileBuffers = await gridFsStream2Buffer(downloadStream);
...@@ -120,7 +120,7 @@ export const deleteRawTextBuffer = async (sourceId: string): Promise<boolean> => ...@@ -120,7 +120,7 @@ export const deleteRawTextBuffer = async (sourceId: string): Promise<boolean> =>
return false; return false;
} }
await gridBucket.delete(buffer._id); await gridBucket.delete(new Types.ObjectId(buffer._id));
return true; return true;
}); });
}; };
...@@ -155,7 +155,7 @@ export const clearExpiredRawTextBufferCron = async () => { ...@@ -155,7 +155,7 @@ export const clearExpiredRawTextBufferCron = async () => {
for (const item of data) { for (const item of data) {
try { try {
await gridBucket.delete(item._id); await gridBucket.delete(new Types.ObjectId(item._id));
} catch (error) { } catch (error) {
addLog.error('Delete expired raw text buffer error', error); addLog.error('Delete expired raw text buffer error', error);
} }
......
...@@ -64,6 +64,33 @@ const addCommonMiddleware = (schema: mongoose.Schema) => { ...@@ -64,6 +64,33 @@ const addCommonMiddleware = (schema: mongoose.Schema) => {
} }
next(); next();
}); });
// Convert _id to string
schema.post(/^find/, function (docs) {
if (!docs) return;
const convertObjectIds = (obj: any) => {
if (!obj) return;
// Convert _id
if (obj._id && obj._id.toString) {
obj._id = obj._id.toString();
}
// Convert other ObjectId fields
Object.keys(obj).forEach((key) => {
if (obj[key] && obj[key]._bsontype === 'ObjectId') {
obj[key] = obj[key].toString();
}
});
};
if (Array.isArray(docs)) {
docs.forEach((doc) => convertObjectIds(doc));
} else {
convertObjectIds(docs);
}
});
}); });
return schema; return schema;
......
...@@ -4,3 +4,10 @@ export const readFromSecondary = { ...@@ -4,3 +4,10 @@ export const readFromSecondary = {
readPreference: ReadPreference.SECONDARY_PREFERRED, // primary | primaryPreferred | secondary | secondaryPreferred | nearest readPreference: ReadPreference.SECONDARY_PREFERRED, // primary | primaryPreferred | secondary | secondaryPreferred | nearest
readConcern: 'local' as any // local | majority | linearizable | available readConcern: 'local' as any // local | majority | linearizable | available
}; };
export const writePrimary = {
writeConcern: {
w: 1,
journal: false
}
};
...@@ -56,3 +56,20 @@ export const delRedisCache = async (key: string) => { ...@@ -56,3 +56,20 @@ export const delRedisCache = async (key: string) => {
const redis = getGlobalRedisConnection(); const redis = getGlobalRedisConnection();
await retryFn(() => redis.del(getCacheKey(key))); await retryFn(() => redis.del(getCacheKey(key)));
}; };
export const appendRedisCache = async (
key: string,
value: string | Buffer | number,
expireSeconds?: number
) => {
try {
const redis = getGlobalRedisConnection();
await retryFn(() => redis.append(getCacheKey(key), value));
if (expireSeconds) {
await redis.expire(getCacheKey(key), expireSeconds);
}
} catch (error) {
addLog.error('Append cache error:', error);
return Promise.reject(error);
}
};
...@@ -2,9 +2,9 @@ import type { NextApiResponse } from 'next'; ...@@ -2,9 +2,9 @@ import type { NextApiResponse } from 'next';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { proxyError, ERROR_RESPONSE, ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import { proxyError, ERROR_RESPONSE, ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
import { addLog } from '../system/log'; import { addLog } from '../system/log';
import { clearCookie } from '../../support/permission/controller';
import { replaceSensitiveText } from '@fastgpt/global/common/string/tools'; import { replaceSensitiveText } from '@fastgpt/global/common/string/tools';
import { UserError } from '@fastgpt/global/common/error/utils'; import { UserError } from '@fastgpt/global/common/error/utils';
import { clearCookie } from '../../support/permission/auth/common';
export interface ResponseType<T = any> { export interface ResponseType<T = any> {
code: number; code: number;
......
...@@ -645,4 +645,4 @@ const createChatCompletion = async ({ ...@@ -645,4 +645,4 @@ const createChatCompletion = async ({
} }
return Promise.reject(error); return Promise.reject(error);
} }
}; };
\ No newline at end of file
...@@ -157,23 +157,18 @@ export const onDelOneApp = async ({ ...@@ -157,23 +157,18 @@ export const onDelOneApp = async ({
).lean(); ).lean();
await Promise.all(evalJobs.map((evalJob) => removeEvaluationJob(evalJob._id))); await Promise.all(evalJobs.map((evalJob) => removeEvaluationJob(evalJob._id)));
// Delete chats
await deleteChatFiles({ appId });
await MongoChatItem.deleteMany({
appId
});
await MongoChat.deleteMany({
appId
});
const del = async (session: ClientSession) => { const del = async (session: ClientSession) => {
for await (const app of apps) { for await (const app of apps) {
const appId = app._id; const appId = app._id;
// Chats
await deleteChatFiles({ appId });
await MongoChatItem.deleteMany(
{
appId
},
{ session }
);
await MongoChat.deleteMany(
{
appId
},
{ session }
);
// 删除分享链接 // 删除分享链接
await MongoOutLink.deleteMany({ await MongoOutLink.deleteMany({
...@@ -205,6 +200,7 @@ export const onDelOneApp = async ({ ...@@ -205,6 +200,7 @@ export const onDelOneApp = async ({
{ $pull: { quickAppIds: { id: String(appId) } } } { $pull: { quickAppIds: { id: String(appId) } } }
).session(session); ).session(session);
// Del permission
await MongoResourcePermission.deleteMany({ await MongoResourcePermission.deleteMany({
resourceType: PerResourceTypeEnum.app, resourceType: PerResourceTypeEnum.app,
teamId, teamId,
......
...@@ -15,6 +15,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; ...@@ -15,6 +15,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils'; import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
import { MongoAppChatLog } from '../app/logs/chatLogsSchema'; import { MongoAppChatLog } from '../app/logs/chatLogsSchema';
import { writePrimary } from '../../common/mongo/utils';
type Props = { type Props = {
chatId: string; chatId: string;
...@@ -115,7 +116,7 @@ export async function saveChat({ ...@@ -115,7 +116,7 @@ export async function saveChat({
}); });
await mongoSessionRun(async (session) => { await mongoSessionRun(async (session) => {
const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.insertMany( const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.create(
processedContent.map((item) => ({ processedContent.map((item) => ({
chatId, chatId,
teamId, teamId,
...@@ -123,7 +124,7 @@ export async function saveChat({ ...@@ -123,7 +124,7 @@ export async function saveChat({
appId, appId,
...item ...item
})), })),
{ session } { session, ordered: true, ...writePrimary }
); );
await MongoChat.updateOne( await MongoChat.updateOne(
...@@ -152,7 +153,8 @@ export async function saveChat({ ...@@ -152,7 +153,8 @@ export async function saveChat({
}, },
{ {
session, session,
upsert: true upsert: true,
...writePrimary
} }
); );
...@@ -215,7 +217,8 @@ export async function saveChat({ ...@@ -215,7 +217,8 @@ export async function saveChat({
} }
}, },
{ {
upsert: true upsert: true,
...writePrimary
} }
); );
} catch (error) { } catch (error) {
...@@ -223,9 +226,15 @@ export async function saveChat({ ...@@ -223,9 +226,15 @@ export async function saveChat({
} }
if (isUpdateUseTime) { if (isUpdateUseTime) {
await MongoApp.findByIdAndUpdate(appId, { await MongoApp.updateOne(
updateTime: new Date() { _id: appId },
}).catch(); {
updateTime: new Date()
},
{
...writePrimary
}
).catch();
} }
} catch (error) { } catch (error) {
addLog.error(`update chat history error`, error); addLog.error(`update chat history error`, error);
......
...@@ -142,7 +142,7 @@ export const clearExpiredDatasetImageCron = async () => { ...@@ -142,7 +142,7 @@ export const clearExpiredDatasetImageCron = async () => {
for (const item of data) { for (const item of data) {
try { try {
await gridBucket.delete(item._id); await gridBucket.delete(new Types.ObjectId(item._id));
} catch (error) { } catch (error) {
addLog.error('Delete expired dataset image error', error); addLog.error('Delete expired dataset image error', error);
} }
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
"@vercel/otel": "^1.13.0", "@vercel/otel": "^1.13.0",
"@xmldom/xmldom": "^0.8.10", "@xmldom/xmldom": "^0.8.10",
"@zilliz/milvus2-sdk-node": "2.4.10", "@zilliz/milvus2-sdk-node": "2.4.10",
"axios": "^1.8.2", "axios": "^1.12.1",
"bullmq": "^5.52.2", "bullmq": "^5.52.2",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"cheerio": "1.0.0-rc.12", "cheerio": "1.0.0-rc.12",
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
"mongoose": "^8.10.1", "mongoose": "^8.10.1",
"multer": "2.0.2", "multer": "2.0.2",
"mysql2": "^3.11.3", "mysql2": "^3.11.3",
"next": "14.2.28", "next": "14.2.32",
"nextjs-cors": "^2.2.0", "nextjs-cors": "^2.2.0",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"node-xlsx": "^0.24.0", "node-xlsx": "^0.24.0",
......
/* Auth app permission */ /* Auth app permission */
import { MongoApp } from '../../../core/app/schema'; import { MongoApp } from '../../../core/app/schema';
import { type AppDetailType } from '@fastgpt/global/core/app/type.d'; import { type AppDetailType } from '@fastgpt/global/core/app/type.d';
import { parseHeaderCert } from '../controller';
import { import {
NullRoleVal,
PerResourceTypeEnum, PerResourceTypeEnum,
ReadPermissionVal, ReadPermissionVal,
ReadRoleVal ReadRoleVal
} from '@fastgpt/global/support/permission/constant'; } from '@fastgpt/global/support/permission/constant';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { getTmbInfoByTmbId } from '../../user/team/controller'; import { getTmbInfoByTmbId } from '../../user/team/controller';
import { getResourcePermission } from '../controller'; import { getTmbPermission } from '../controller';
import { AppPermission } from '@fastgpt/global/support/permission/app/controller'; import { AppPermission } from '@fastgpt/global/support/permission/app/controller';
import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { type PermissionValueType } from '@fastgpt/global/support/permission/type';
import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants';
...@@ -18,6 +18,8 @@ import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; ...@@ -18,6 +18,8 @@ import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants';
import { type AuthModeType, type AuthResponseType } from '../type'; import { type AuthModeType, type AuthResponseType } from '../type';
import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils'; import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils';
import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant'; import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant';
import { parseHeaderCert } from '../auth/common';
import { sumPer } from '@fastgpt/global/support/permission/utils';
export const authPluginByTmbId = async ({ export const authPluginByTmbId = async ({
tmbId, tmbId,
...@@ -90,53 +92,27 @@ export const authAppByTmbId = async ({ ...@@ -90,53 +92,27 @@ export const authAppByTmbId = async ({
const isOwner = tmbPer.isOwner || String(app.tmbId) === String(tmbId); const isOwner = tmbPer.isOwner || String(app.tmbId) === String(tmbId);
const { Per } = await (async () => { const isGetParentClb =
if (isOwner) { app.inheritPermission && !AppFolderTypeList.includes(app.type) && !!app.parentId;
return {
Per: new AppPermission({ isOwner: true }) const [folderPer = NullRoleVal, myPer = NullRoleVal] = await Promise.all([
}; isGetParentClb
} ? getTmbPermission({
teamId,
if ( tmbId,
AppFolderTypeList.includes(app.type) || resourceId: app.parentId!,
app.inheritPermission === false || resourceType: PerResourceTypeEnum.app
!app.parentId })
) { : NullRoleVal,
// 1. is a folder. (Folders have completely permission) getTmbPermission({
// 2. inheritPermission is false. teamId,
// 3. is root folder/app. tmbId,
const role = await getResourcePermission({ resourceId: appId,
teamId, resourceType: PerResourceTypeEnum.app
tmbId, })
resourceId: appId, ]);
resourceType: PerResourceTypeEnum.app
}); const Per = new AppPermission({ role: sumPer(folderPer, myPer), isOwner });
const Per = new AppPermission({ role, isOwner });
if (app.favourite || app.quick) {
Per.addRole(ReadRoleVal);
}
return {
Per
};
} else {
// is not folder and inheritPermission is true and is not root folder.
const { app: parent } = await authAppByTmbId({
tmbId,
appId: app.parentId,
per
});
const Per = new AppPermission({
role: parent.permission.role,
isOwner
});
return {
Per
};
}
})();
if (!Per.checkPer(per)) { if (!Per.checkPer(per)) {
return Promise.reject(AppErrEnum.unAuthApp); return Promise.reject(AppErrEnum.unAuthApp);
......
import { parseHeaderCert } from '../controller'; import type { ReqHeaderAuthType } from '../type';
import { type AuthModeType } from '../type'; import { type AuthModeType } from '../type';
import { SERVICE_LOCAL_HOST } from '../../../common/system/tools'; import { SERVICE_LOCAL_HOST } from '../../../common/system/tools';
import { type ApiRequestProps } from '../../../type/next'; import { type ApiRequestProps } from '../../../type/next';
import type { NextApiResponse } from 'next';
import Cookie from 'cookie';
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
import { authUserSession } from '../../../support/user/session';
import { authOpenApiKey } from '../../../support/openapi/auth';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
export const authCert = async (props: AuthModeType) => { export const authCert = async (props: AuthModeType) => {
const result = await parseHeaderCert(props); const result = await parseHeaderCert(props);
...@@ -19,3 +25,149 @@ export const authRequestFromLocal = ({ req }: { req: ApiRequestProps }) => { ...@@ -19,3 +25,149 @@ export const authRequestFromLocal = ({ req }: { req: ApiRequestProps }) => {
return Promise.reject('Invalid request'); return Promise.reject('Invalid request');
} }
}; };
export async function parseHeaderCert({
req,
authToken = false,
authRoot = false,
authApiKey = false
}: AuthModeType) {
// parse jwt
async function authCookieToken(cookie?: string, token?: string) {
// 获取 cookie
const cookies = Cookie.parse(cookie || '');
const cookieToken = token || cookies[TokenName];
if (!cookieToken) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
return { ...(await authUserSession(cookieToken)), sessionId: cookieToken };
}
// from authorization get apikey
async function parseAuthorization(authorization?: string) {
if (!authorization) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
// Bearer fastgpt-xxxx-appId
const auth = authorization.split(' ')[1];
if (!auth) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
const { apikey, appId: authorizationAppid = '' } = await (async () => {
const arr = auth.split('-');
// abandon
if (arr.length === 3) {
return {
apikey: `${arr[0]}-${arr[1]}`,
appId: arr[2]
};
}
if (arr.length === 2) {
return {
apikey: auth
};
}
return Promise.reject(ERROR_ENUM.unAuthorization);
})();
// auth apikey
const { teamId, tmbId, appId: apiKeyAppId = '', sourceName } = await authOpenApiKey({ apikey });
return {
uid: '',
teamId,
tmbId,
apikey,
appId: apiKeyAppId || authorizationAppid,
sourceName
};
}
// root user
async function parseRootKey(rootKey?: string) {
if (!rootKey || !process.env.ROOT_KEY || rootKey !== process.env.ROOT_KEY) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
}
const { cookie, token, rootkey, authorization } = (req.headers || {}) as ReqHeaderAuthType;
const { uid, teamId, tmbId, appId, openApiKey, authType, isRoot, sourceName, sessionId } =
await (async () => {
if (authApiKey && authorization) {
// apikey from authorization
const authResponse = await parseAuthorization(authorization);
return {
uid: authResponse.uid,
teamId: authResponse.teamId,
tmbId: authResponse.tmbId,
appId: authResponse.appId,
openApiKey: authResponse.apikey,
authType: AuthUserTypeEnum.apikey,
sourceName: authResponse.sourceName
};
}
if (authToken && (token || cookie)) {
// user token(from fastgpt web)
const res = await authCookieToken(cookie, token);
return {
uid: res.userId,
teamId: res.teamId,
tmbId: res.tmbId,
appId: '',
openApiKey: '',
authType: AuthUserTypeEnum.token,
isRoot: res.isRoot,
sessionId: res.sessionId
};
}
if (authRoot && rootkey) {
await parseRootKey(rootkey);
// root user
return {
uid: '',
teamId: '',
tmbId: '',
appId: '',
openApiKey: '',
authType: AuthUserTypeEnum.root,
isRoot: true
};
}
return Promise.reject(ERROR_ENUM.unAuthorization);
})();
if (!authRoot && (!teamId || !tmbId)) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
return {
userId: String(uid),
teamId: String(teamId),
tmbId: String(tmbId),
appId,
authType,
sourceName,
apikey: openApiKey,
isRoot: !!isRoot,
sessionId
};
}
/* set cookie */
export const TokenName = 'fastgpt_token';
export const setCookie = (res: NextApiResponse, token: string) => {
res.setHeader(
'Set-Cookie',
`${TokenName}=${token}; Path=/; HttpOnly; Max-Age=604800; Samesite=Strict;`
);
};
/* clear cookie */
export const clearCookie = (res: NextApiResponse) => {
res.setHeader('Set-Cookie', `${TokenName}=; Path=/; Max-Age=0`);
};
import { type AuthModeType, type AuthResponseType } from '../type'; import { type AuthModeType, type AuthResponseType } from '../type';
import { type DatasetFileSchema } from '@fastgpt/global/core/dataset/type'; import { type DatasetFileSchema } from '@fastgpt/global/core/dataset/type';
import { parseHeaderCert } from '../controller';
import { getFileById } from '../../../common/file/gridfs/controller'; import { getFileById } from '../../../common/file/gridfs/controller';
import { BucketNameEnum } from '@fastgpt/global/common/file/constants'; import { BucketNameEnum, bucketNameMap } from '@fastgpt/global/common/file/constants';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { OwnerPermissionVal, ReadRoleVal } from '@fastgpt/global/support/permission/constant'; import { OwnerPermissionVal, ReadRoleVal } from '@fastgpt/global/support/permission/constant';
import { Permission } from '@fastgpt/global/support/permission/controller'; import { Permission } from '@fastgpt/global/support/permission/controller';
import type { FileTokenQuery } from '@fastgpt/global/common/file/type';
import { addMinutes } from 'date-fns';
import { parseHeaderCert } from './common';
import jwt from 'jsonwebtoken';
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
export const authCollectionFile = async ({ export const authCollectionFile = async ({
fileId, fileId,
...@@ -46,3 +50,45 @@ export const authCollectionFile = async ({ ...@@ -46,3 +50,45 @@ export const authCollectionFile = async ({
file file
}; };
}; };
/* file permission */
export const createFileToken = (data: FileTokenQuery) => {
if (!process.env.FILE_TOKEN_KEY) {
return Promise.reject('System unset FILE_TOKEN_KEY');
}
const expireMinutes =
data.customExpireMinutes ?? bucketNameMap[data.bucketName].previewExpireMinutes;
const expiredTime = Math.floor(addMinutes(new Date(), expireMinutes).getTime() / 1000);
const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken';
const token = jwt.sign(
{
...data,
exp: expiredTime
},
key
);
return Promise.resolve(token);
};
export const authFileToken = (token?: string) =>
new Promise<FileTokenQuery>((resolve, reject) => {
if (!token) {
return reject(ERROR_ENUM.unAuthFile);
}
const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken';
jwt.verify(token, key, (err, decoded: any) => {
if (err || !decoded.bucketName || !decoded?.teamId || !decoded?.fileId) {
reject(ERROR_ENUM.unAuthFile);
return;
}
resolve({
bucketName: decoded.bucketName,
teamId: decoded.teamId,
uid: decoded.uid,
fileId: decoded.fileId
});
});
});
import { type AuthModeType, type AuthResponseType } from '../type'; import { type AuthModeType, type AuthResponseType } from '../type';
import { type OpenApiSchema } from '@fastgpt/global/support/openapi/type'; import { type OpenApiSchema } from '@fastgpt/global/support/openapi/type';
import { parseHeaderCert } from '../controller';
import { getTmbInfoByTmbId } from '../../user/team/controller'; import { getTmbInfoByTmbId } from '../../user/team/controller';
import { MongoOpenApi } from '../../openapi/schema'; import { MongoOpenApi } from '../../openapi/schema';
import { OpenApiErrEnum } from '@fastgpt/global/common/error/code/openapi'; import { OpenApiErrEnum } from '@fastgpt/global/common/error/code/openapi';
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant'; import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
import { authAppByTmbId } from '../app/auth'; import { authAppByTmbId } from '../app/auth';
import { parseHeaderCert } from './common';
export async function authOpenApiKeyCrud({ export async function authOpenApiKeyCrud({
id, id,
......
import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { type PermissionValueType } from '@fastgpt/global/support/permission/type';
import { getResourcePermission, parseHeaderCert } from '../controller'; import { getTmbPermission } from '../controller';
import { import {
type CollectionWithDatasetType, type CollectionWithDatasetType,
type DatasetDataItemType, type DatasetDataItemType,
...@@ -9,6 +9,7 @@ import { getTmbInfoByTmbId } from '../../user/team/controller'; ...@@ -9,6 +9,7 @@ import { getTmbInfoByTmbId } from '../../user/team/controller';
import { MongoDataset } from '../../../core/dataset/schema'; import { MongoDataset } from '../../../core/dataset/schema';
import { import {
NullPermissionVal, NullPermissionVal,
NullRoleVal,
PerResourceTypeEnum PerResourceTypeEnum
} from '@fastgpt/global/support/permission/constant'; } from '@fastgpt/global/support/permission/constant';
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset'; import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
...@@ -21,6 +22,8 @@ import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; ...@@ -21,6 +22,8 @@ import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { DataSetDefaultRoleVal } from '@fastgpt/global/support/permission/dataset/constant'; import { DataSetDefaultRoleVal } from '@fastgpt/global/support/permission/dataset/constant';
import { getDatasetImagePreviewUrl } from '../../../core/dataset/image/utils'; import { getDatasetImagePreviewUrl } from '../../../core/dataset/image/utils';
import { i18nT } from '../../../../web/i18n/utils'; import { i18nT } from '../../../../web/i18n/utils';
import { parseHeaderCert } from '../auth/common';
import { sumPer } from '@fastgpt/global/support/permission/utils';
export const authDatasetByTmbId = async ({ export const authDatasetByTmbId = async ({
tmbId, tmbId,
...@@ -61,54 +64,27 @@ export const authDatasetByTmbId = async ({ ...@@ -61,54 +64,27 @@ export const authDatasetByTmbId = async ({
} }
const isOwner = tmbPer.isOwner || String(dataset.tmbId) === String(tmbId); const isOwner = tmbPer.isOwner || String(dataset.tmbId) === String(tmbId);
const isGetParentClb =
dataset.inheritPermission && dataset.type !== DatasetTypeEnum.folder && !!dataset.parentId;
// get dataset permission or inherit permission from parent folder. const [folderPer = NullRoleVal, myPer = NullRoleVal] = await Promise.all([
const { Per } = await (async () => { isGetParentClb
if (isOwner) { ? getTmbPermission({
return { teamId,
Per: new DatasetPermission({ isOwner: true }) tmbId,
}; resourceId: dataset.parentId!,
} resourceType: PerResourceTypeEnum.dataset
if ( })
dataset.type === DatasetTypeEnum.folder || : NullRoleVal,
dataset.inheritPermission === false || getTmbPermission({
!dataset.parentId teamId,
) { tmbId,
// 1. is a folder. (Folders have completely permission) resourceId: datasetId,
// 2. inheritPermission is false. resourceType: PerResourceTypeEnum.dataset
// 3. is root folder/dataset. })
const rp = await getResourcePermission({ ]);
teamId,
tmbId,
resourceId: datasetId,
resourceType: PerResourceTypeEnum.dataset
});
const Per = new DatasetPermission({
role: rp,
isOwner
});
return {
Per
};
} else {
// is not folder and inheritPermission is true and is not root folder.
const { dataset: parent } = await authDatasetByTmbId({
tmbId,
datasetId: dataset.parentId,
per,
isRoot
});
const Per = new DatasetPermission({
role: parent.permission.role,
isOwner
});
return { const Per = new DatasetPermission({ role: sumPer(folderPer, myPer), isOwner });
Per
};
}
})();
if (!Per.checkPer(per)) { if (!Per.checkPer(per)) {
return Promise.reject(DatasetErrEnum.unAuthDataset); return Promise.reject(DatasetErrEnum.unAuthDataset);
......
import { parseHeaderCert } from '../controller';
import { authAppByTmbId } from '../app/auth'; import { authAppByTmbId } from '../app/auth';
import { import {
ManagePermissionVal, ManagePermissionVal,
...@@ -7,6 +6,7 @@ import { ...@@ -7,6 +6,7 @@ import {
import type { EvaluationSchemaType } from '@fastgpt/global/core/app/evaluation/type'; import type { EvaluationSchemaType } from '@fastgpt/global/core/app/evaluation/type';
import type { AuthModeType } from '../type'; import type { AuthModeType } from '../type';
import { MongoEvaluation } from '../../../core/app/evaluation/evalSchema'; import { MongoEvaluation } from '../../../core/app/evaluation/evalSchema';
import { parseHeaderCert } from '../auth/common';
export const authEval = async ({ export const authEval = async ({
evalId, evalId,
......
import { type MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; import { type MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type';
import { MongoGroupMemberModel } from './groupMemberSchema'; import { MongoGroupMemberModel } from './groupMemberSchema';
import { parseHeaderCert } from '../controller';
import { MongoMemberGroupModel } from './memberGroupSchema'; import { MongoMemberGroupModel } from './memberGroupSchema';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { type ClientSession } from 'mongoose'; import { type ClientSession } from 'mongoose';
...@@ -9,6 +8,7 @@ import { type AuthModeType, type AuthResponseType } from '../type'; ...@@ -9,6 +8,7 @@ import { type AuthModeType, type AuthResponseType } from '../type';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller';
import { getTmbInfoByTmbId } from '../../user/team/controller'; import { getTmbInfoByTmbId } from '../../user/team/controller';
import { parseHeaderCert } from '../auth/common';
/** /**
* Get the default group of a team * Get the default group of a team
......
...@@ -4,6 +4,7 @@ import type { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; ...@@ -4,6 +4,7 @@ import type { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type';
import { connectionMongo, getMongoModel } from '../../../common/mongo'; import { connectionMongo, getMongoModel } from '../../../common/mongo';
import { OrgMemberCollectionName } from './orgMemberSchema'; import { OrgMemberCollectionName } from './orgMemberSchema';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants';
const { Schema } = connectionMongo; const { Schema } = connectionMongo;
export const OrgSchema = new Schema( export const OrgSchema = new Schema(
...@@ -29,7 +30,9 @@ export const OrgSchema = new Schema( ...@@ -29,7 +30,9 @@ export const OrgSchema = new Schema(
type: String, type: String,
required: true required: true
}, },
avatar: String, avatar: {
type: String
},
description: String, description: String,
updateTime: { updateTime: {
type: Date, type: Date,
......
import { type AppDetailType } from '@fastgpt/global/core/app/type'; import { type AppDetailType } from '@fastgpt/global/core/app/type';
import { type OutlinkAppType, type OutLinkSchema } from '@fastgpt/global/support/outLink/type'; import { type OutlinkAppType, type OutLinkSchema } from '@fastgpt/global/support/outLink/type';
import { parseHeaderCert } from '../controller';
import { MongoOutLink } from '../../outLink/schema'; import { MongoOutLink } from '../../outLink/schema';
import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink'; import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink';
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant'; import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
import { authAppByTmbId } from '../app/auth'; import { authAppByTmbId } from '../app/auth';
import { type AuthModeType, type AuthResponseType } from '../type'; import { type AuthModeType, type AuthResponseType } from '../type';
import { parseHeaderCert } from '../auth/common';
/* crud outlink permission */ /* crud outlink permission */
export async function authOutLinkCrud({ export async function authOutLinkCrud({
......
...@@ -34,11 +34,18 @@ export const ResourcePermissionSchema = new Schema({ ...@@ -34,11 +34,18 @@ export const ResourcePermissionSchema = new Schema({
enum: Object.values(PerResourceTypeEnum), enum: Object.values(PerResourceTypeEnum),
required: true required: true
}, },
/**
* The **Role** of the object to the resource.
*/
permission: { permission: {
type: Number, type: Number,
required: true required: true
}, },
// Resrouce ID: App or DataSet or any other resource type. /**
* Optional. Only be set when the resource is *inherited* from the parent resource.
* For recording the self permission. When cancel the inheritance, it will overwrite the permission property and set to `unset`.
*/
// Resource ID: App or DataSet or any other resource type.
// It is null if the resourceType is team. // It is null if the resourceType is team.
resourceId: { resourceId: {
type: Schema.Types.ObjectId type: Schema.Types.ObjectId
......
import { type TeamTmbItemType } from '@fastgpt/global/support/user/team/type'; import { type TeamTmbItemType } from '@fastgpt/global/support/user/team/type';
import { parseHeaderCert } from '../controller';
import { getTmbInfoByTmbId } from '../../user/team/controller'; import { getTmbInfoByTmbId } from '../../user/team/controller';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { type AuthModeType, type AuthResponseType } from '../type'; import { type AuthModeType, type AuthResponseType } from '../type';
import { NullPermissionVal } from '@fastgpt/global/support/permission/constant'; import { NullPermissionVal } from '@fastgpt/global/support/permission/constant';
import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller';
import { authCert } from '../auth/common'; import { authCert, parseHeaderCert } from '../auth/common';
import { MongoUser } from '../../user/schema'; import { MongoUser } from '../../user/schema';
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
import { type ApiRequestProps } from '../../../type/next'; import { type ApiRequestProps } from '../../../type/next';
......
import type { CollaboratorIdType } from '@fastgpt/global/support/permission/collaborator';
export const pickCollaboratorIdFields = (clb: CollaboratorIdType) => {
return {
...(clb.tmbId && { tmbId: clb.tmbId }),
...(clb.groupId && { groupId: clb.groupId }),
...(clb.orgId && { orgId: clb.orgId })
};
};
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
import { MongoTeamMember } from './teamMemberSchema'; import { MongoTeamMember } from './teamMemberSchema';
import { MongoTeam } from './teamSchema'; import { MongoTeam } from './teamSchema';
import { type UpdateTeamProps } from '@fastgpt/global/support/user/team/controller'; import { type UpdateTeamProps } from '@fastgpt/global/support/user/team/controller';
import { getResourcePermission } from '../../permission/controller'; import { getTmbPermission } from '../../permission/controller';
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller';
import { TeamDefaultRoleVal } from '@fastgpt/global/support/permission/user/constant'; import { TeamDefaultRoleVal } from '@fastgpt/global/support/permission/user/constant';
...@@ -26,7 +26,7 @@ async function getTeamMember(match: Record<string, any>): Promise<TeamTmbItemTyp ...@@ -26,7 +26,7 @@ async function getTeamMember(match: Record<string, any>): Promise<TeamTmbItemTyp
} }
const role = const role =
(await getResourcePermission({ (await getTmbPermission({
resourceType: PerResourceTypeEnum.team, resourceType: PerResourceTypeEnum.team,
teamId: tmb.teamId, teamId: tmb.teamId,
tmbId: tmb._id tmbId: tmb._id
......
...@@ -274,7 +274,11 @@ const MySelect = <T = any,>( ...@@ -274,7 +274,11 @@ const MySelect = <T = any,>(
w={selectItem.iconSize ?? '1rem'} w={selectItem.iconSize ?? '1rem'}
/> />
)} )}
{selectItem?.alias || selectItem?.label || placeholder} {
<Box noOfLines={1}>
{selectItem?.alias || selectItem?.label || placeholder}
</Box>
}
</> </>
)} )}
</> </>
......
import React from 'react'; import React, { useCallback } from 'react';
import { Box, Flex, useTheme, Grid, type GridProps, HStack } from '@chakra-ui/react'; import { Box, Flex, Grid, type GridProps, HStack } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import QuestionTip from '../MyTooltip/QuestionTip'; import QuestionTip from '../MyTooltip/QuestionTip';
...@@ -16,57 +16,83 @@ type Props<T> = Omit<GridProps, 'onChange'> & { ...@@ -16,57 +16,83 @@ type Props<T> = Omit<GridProps, 'onChange'> & {
defaultBg?: string; defaultBg?: string;
activeBg?: string; activeBg?: string;
onChange: (e: T) => void; onChange: (e: T) => void;
isDisabled?: boolean;
}; };
const LeftRadio = <T = any,>({ const LeftRadio = <T = any,>({
list, list,
value, value,
align = 'flex-top', align = 'center',
px = 3.5, px = 3.5,
py = 4, py = 4,
defaultBg = 'myGray.50', defaultBg = 'myGray.50',
activeBg = 'primary.50', activeBg = 'primary.50',
onChange, onChange,
isDisabled = false,
...props ...props
}: Props<T>) => { }: Props<T>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme();
const getBoxStyle = useCallback(
(isActive: boolean) => {
const baseStyle = {
px,
py,
border: 'base',
borderWidth: '1px',
borderRadius: 'md'
};
if (isActive) {
return {
...baseStyle,
borderColor: 'primary.400',
bg: activeBg,
boxShadow: 'focus',
cursor: 'pointer',
opacity: 1
};
}
if (isDisabled) {
return {
...baseStyle,
bg: 'myWhite.300',
borderColor: 'myGray.200',
color: 'myGray.500',
cursor: 'not-allowed',
opacity: 0.6
};
}
return {
...baseStyle,
bg: defaultBg,
_hover: { borderColor: 'primary.300' },
cursor: 'pointer',
opacity: 1
};
},
[activeBg, defaultBg, isDisabled, px, py]
);
return ( return (
<Grid gridGap={[3, 5]} fontSize={['sm', 'md']} {...props}> <Grid gridGap={[3, 5]} fontSize={['sm', 'md']} {...props}>
{list.map((item) => ( {list.map((item) => {
<Box const isActive = value === item.value;
key={item.value as any} return (
cursor={'pointer'} <Box
userSelect={'none'} key={item.value as any}
px={px} position={'relative'}
py={py} userSelect={'none'}
border={'base'} onClick={() => !isDisabled && onChange(item.value)}
borderWidth={'1px'} {...getBoxStyle(isActive)}
borderRadius={'md'} >
position={'relative'} <Flex alignItems={align}>
{...(value === item.value {/* Circle */}
? {
borderColor: list.length > 1 ? 'primary.400' : '',
bg: activeBg,
boxShadow: list.length > 1 ? 'focus' : 'none'
}
: {
bg: defaultBg,
_hover: {
borderColor: 'primary.300'
}
})}
onClick={() => onChange(item.value)}
>
{/* Circle */}
<Flex alignItems={'center'}>
{list.length > 1 && (
<Box <Box
w={'18px'} w={'18px'}
h={'18px'} h={'18px'}
borderWidth={'2.4px'} borderWidth={'2.4px'}
borderColor={value === item.value ? 'primary.015' : 'transparent'} borderColor={isActive ? 'primary.015' : 'transparent'}
borderRadius={'50%'} borderRadius={'50%'}
mr={3} mr={3}
> >
...@@ -74,52 +100,59 @@ const LeftRadio = <T = any,>({ ...@@ -74,52 +100,59 @@ const LeftRadio = <T = any,>({
w={'100%'} w={'100%'}
h={'100%'} h={'100%'}
borderWidth={'1px'} borderWidth={'1px'}
borderColor={value === item.value ? 'primary.600' : 'borderColor.high'}
bg={value === item.value ? 'primary.1' : 'transparent'}
borderRadius={'50%'} borderRadius={'50%'}
alignItems={'center'} alignItems={'center'}
justifyContent={'center'} justifyContent={'center'}
{...(isActive
? {
borderColor: 'primary.600',
bg: 'primary.1'
}
: {
borderColor: 'borderColor.high',
bg: 'transparent'
})}
> >
<Box <Box
w={'5px'} w={'5px'}
h={'5px'} h={'5px'}
borderRadius={'50%'} borderRadius={'50%'}
bg={value === item.value ? 'primary.600' : 'transparent'} bg={isActive ? 'primary.600' : 'transparent'}
></Box> ></Box>
</Flex> </Flex>
</Box> </Box>
)} <Box flex={'1 0 0'}>
<Box flex={'1 0 0'}> {typeof item.title === 'string' ? (
{typeof item.title === 'string' ? ( <HStack
<HStack spacing={1}
spacing={1} fontWeight={item.desc ? 'medium' : 'normal'}
color={'myGray.900'} whiteSpace={'nowrap'}
fontWeight={item.desc ? '500' : 'normal'} fontSize={'sm'}
whiteSpace={'nowrap'} lineHeight={1}
fontSize={'sm'} color={'myGray.900'}
lineHeight={1} >
> <Box>{t(item.title as any)}</Box>
<Box>{t(item.title as any)}</Box> {!!item.tooltip && <QuestionTip label={item.tooltip} color={'myGray.600'} />}
{!!item.tooltip && <QuestionTip label={item.tooltip} color={'myGray.600'} />} </HStack>
</HStack> ) : (
) : ( item.title
item.title )}
)}
{!!item.desc && ( {!!item.desc && (
<Box fontSize={'xs'} color={'myGray.500'} mt={1.5} lineHeight={1.2}> <Box fontSize={'xs'} mt={1.5} lineHeight={1.2}>
{t(item.desc as any)} {t(item.desc as any)}
</Box> </Box>
)} )}
</Box> </Box>
</Flex> </Flex>
{item?.children && ( {item?.children && (
<Box mt={4} pt={4} borderTop={'base'} cursor={'default'}> <Box mt={4} pt={4} borderTop={'base'} cursor={'default'}>
{item?.children} {item?.children}
</Box> </Box>
)} )}
</Box> </Box>
))} );
})}
</Grid> </Grid>
); );
}; };
......
...@@ -15,6 +15,7 @@ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; ...@@ -15,6 +15,7 @@ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { ListPlugin } from '@lexical/react/LexicalListPlugin'; import { ListPlugin } from '@lexical/react/LexicalListPlugin';
import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin'; import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin';
import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'; import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import { HeadingNode, QuoteNode } from '@lexical/rich-text'; import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { ListItemNode, ListNode } from '@lexical/list'; import { ListItemNode, ListNode } from '@lexical/list';
...@@ -219,7 +220,6 @@ export default function Editor({ ...@@ -219,7 +220,6 @@ export default function Editor({
)} )}
{variableLabels.length > 0 && <VariablePickerPlugin variables={variables} />} {variableLabels.length > 0 && <VariablePickerPlugin variables={variables} />}
<OnBlurPlugin onBlur={onBlur} /> <OnBlurPlugin onBlur={onBlur} />
<ListDisplayFixPlugin />
<OnChangePlugin <OnChangePlugin
onChange={(editorState, editor) => { onChange={(editorState, editor) => {
const rootElement = editor.getRootElement(); const rootElement = editor.getRootElement();
...@@ -232,11 +232,12 @@ export default function Editor({ ...@@ -232,11 +232,12 @@ export default function Editor({
{isRichText && ( {isRichText && (
<> <>
{/* <ListPlugin /> <ListDisplayFixPlugin />
<TabIndentationPlugin />
<ListPlugin />
<CheckListPlugin /> <CheckListPlugin />
<ListExitPlugin /> */} <ListExitPlugin />
<TabToSpacesPlugin /> <MarkdownPlugin />
{/* <MarkdownPlugin /> */}
</> </>
)} )}
</> </>
......
...@@ -70,7 +70,7 @@ export default function ListExitPlugin(): JSX.Element | null { ...@@ -70,7 +70,7 @@ export default function ListExitPlugin(): JSX.Element | null {
} }
const anchorNode = selection.anchor.getNode(); const anchorNode = selection.anchor.getNode();
const listItemNode = anchorNode.getParent(); const listItemNode = $isListItemNode(anchorNode) ? anchorNode : anchorNode.getParent();
if ($isListItemNode(listItemNode)) { if ($isListItemNode(listItemNode)) {
// Check if cursor is at the beginning of an empty list item // Check if cursor is at the beginning of an empty list item
......
...@@ -23,3 +23,88 @@ export type EditorVariableLabelPickerType = { ...@@ -23,3 +23,88 @@ export type EditorVariableLabelPickerType = {
}; };
export type FormPropsType = Omit<BoxProps, 'onChange' | 'onBlur'>; export type FormPropsType = Omit<BoxProps, 'onChange' | 'onBlur'>;
// Lexical editor node types
export type BaseEditorNode = {
type: string;
version: number;
};
export type TextEditorNode = BaseEditorNode & {
type: 'text';
text: string;
detail: number;
format: number;
mode: string;
style: string;
};
export type LineBreakEditorNode = BaseEditorNode & {
type: 'linebreak';
};
export type VariableLabelEditorNode = BaseEditorNode & {
type: 'variableLabel';
variableKey: string;
};
export type VariableEditorNode = BaseEditorNode & {
type: 'Variable';
variableKey: string;
};
export type TabEditorNode = BaseEditorNode & {
type: 'tab';
};
export type ChildEditorNode =
| TextEditorNode
| LineBreakEditorNode
| VariableLabelEditorNode
| VariableEditorNode
| TabEditorNode;
export type ParagraphEditorNode = BaseEditorNode & {
type: 'paragraph';
children: ChildEditorNode[];
direction: string;
format: string;
indent: number;
};
export type ListItemEditorNode = BaseEditorNode & {
type: 'listitem';
children: Array<ChildEditorNode | ListEditorNode>;
direction: string | null;
format: string;
indent: number;
value: number;
};
export type ListEditorNode = BaseEditorNode & {
type: 'list';
children: ListItemEditorNode[];
direction: string | null;
format: string;
indent: number;
listType: 'bullet' | 'number';
start: number;
tag: 'ul' | 'ol';
};
export type EditorState = {
root: {
type: 'root';
children: Array<ParagraphEditorNode | ListEditorNode>;
direction: string;
format: string;
indent: number;
} & BaseEditorNode;
};
export type ListItemInfo = {
type: 'bullet' | 'number';
text: string;
indent: number;
numberValue?: number;
};
...@@ -6,12 +6,19 @@ ...@@ -6,12 +6,19 @@
* *
*/ */
import type { DecoratorNode, Klass, LexicalEditor, LexicalNode } from 'lexical'; import type { Klass, LexicalEditor, LexicalNode } from 'lexical';
import type { EntityMatch } from '@lexical/text'; import type { EntityMatch } from '@lexical/text';
import { $createTextNode, $isTextNode, TextNode } from 'lexical'; import { $createTextNode, $isTextNode, TextNode } from 'lexical';
import { useCallback } from 'react'; import { useCallback } from 'react';
import type { VariableLabelNode } from './plugins/VariableLabelPlugin/node'; import type { VariableLabelNode } from './plugins/VariableLabelPlugin/node';
import type { VariableNode } from './plugins/VariablePlugin/node'; import type { VariableNode } from './plugins/VariablePlugin/node';
import type {
ListItemEditorNode,
ListEditorNode,
ParagraphEditorNode,
EditorState,
ListItemInfo
} from './type';
export function registerLexicalTextEntity<T extends TextNode | VariableLabelNode | VariableNode>( export function registerLexicalTextEntity<T extends TextNode | VariableLabelNode | VariableNode>(
editor: LexicalEditor, editor: LexicalEditor,
...@@ -175,31 +182,148 @@ export function registerLexicalTextEntity<T extends TextNode | VariableLabelNode ...@@ -175,31 +182,148 @@ export function registerLexicalTextEntity<T extends TextNode | VariableLabelNode
return [removePlainTextTransform, removeReverseNodeTransform]; return [removePlainTextTransform, removeReverseNodeTransform];
} }
export function textToEditorState(text = '') { // text to editor state
const paragraph = typeof text === 'string' ? text?.split('\n') : ['']; const parseTextLine = (line: string) => {
const trimmed = line.trimStart();
const indentLevel = Math.floor((line.length - trimmed.length) / 2);
const bulletMatch = trimmed.match(/^- (.*)$/);
if (bulletMatch) {
return { type: 'bullet', text: bulletMatch[1], indent: indentLevel };
}
const numberMatch = trimmed.match(/^(\d+)\. (.*)$/);
if (numberMatch) {
return {
type: 'number',
text: numberMatch[2],
indent: indentLevel,
numberValue: parseInt(numberMatch[1])
};
}
return { type: 'paragraph', text: trimmed, indent: indentLevel };
};
const buildListStructure = (items: ListItemInfo[]) => {
const result: ListEditorNode[] = [];
let i = 0;
while (i < items.length) {
const currentListType = items[i].type;
const currentIndent = items[i].indent;
const currentListItems: ListItemEditorNode[] = [];
// Collect consecutive items of the same type
while (i < items.length && items[i].type === currentListType) {
const listItem: ListItemEditorNode = {
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: items[i].text,
type: 'text' as const,
version: 1
}
],
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem' as const,
version: 1,
value: items[i].numberValue || 1
};
// Collect nested items
const nestedItems: ListItemInfo[] = [];
let j = i + 1;
while (j < items.length && items[j].indent > currentIndent) {
nestedItems.push(items[j]);
j++;
}
// recursively build nested lists and add them to the current item's children
if (nestedItems.length > 0) {
const nestedLists = buildListStructure(nestedItems);
listItem.children.push(...nestedLists);
}
currentListItems.push(listItem);
i = j;
}
result.push({
children: currentListItems,
direction: 'ltr',
format: '',
indent: 0,
type: 'list' as const,
version: 1,
listType: currentListType,
start: 1,
tag: currentListType === 'bullet' ? 'ul' : ('ol' as const)
});
}
return result;
};
export const textToEditorState = (text = '') => {
const lines = text.split('\n');
const children: Array<ParagraphEditorNode | ListEditorNode> = [];
let i = 0;
while (i < lines.length) {
const parsed = parseTextLine(lines[i]);
if (parsed.type === 'paragraph') {
children.push({
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: parsed.text,
type: 'text',
version: 1
}
],
direction: 'ltr',
format: '',
indent: parsed.indent,
type: 'paragraph',
version: 1
});
i++;
} else {
const listItems: ListItemInfo[] = [];
while (i < lines.length) {
const currentParsed = parseTextLine(lines[i]);
if (currentParsed.type === 'paragraph') {
break;
}
listItems.push({
type: currentParsed.type as 'bullet' | 'number',
text: currentParsed.text,
indent: currentParsed.indent,
numberValue: currentParsed.numberValue
});
i++;
}
// build nested lists and add to children
const lists = buildListStructure(listItems) as ListEditorNode[];
children.push(...lists);
}
}
return JSON.stringify({ return JSON.stringify({
root: { root: {
children: paragraph.map((p) => { children: children,
return {
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: p,
type: 'text',
version: 1
}
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1
};
}),
direction: 'ltr', direction: 'ltr',
format: '', format: '',
indent: 0, indent: 0,
...@@ -207,30 +331,9 @@ export function textToEditorState(text = '') { ...@@ -207,30 +331,9 @@ export function textToEditorState(text = '') {
version: 1 version: 1
} }
}); });
}
const varRegex = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
export const getVars = (value: string) => {
if (!value) return [];
const keys =
value
.match(varRegex)
?.map((item) => {
return item.replace('{{', '').replace('}}', '');
})
.filter((key) => key.length <= 10) || [];
const keyObj: Record<string, boolean> = {};
// remove duplicate keys
const res: string[] = [];
keys.forEach((key) => {
if (keyObj[key]) return;
keyObj[key] = true;
res.push(key);
});
return res;
}; };
// menu text match
export type MenuTextMatch = { export type MenuTextMatch = {
leadOffset: number; leadOffset: number;
matchingString: string; matchingString: string;
...@@ -266,22 +369,102 @@ export function useBasicTypeaheadTriggerMatch( ...@@ -266,22 +369,102 @@ export function useBasicTypeaheadTriggerMatch(
); );
} }
export function editorStateToText(editor: LexicalEditor) { // editor state to text
const editorStateTextString: string[] = []; const processListItem = ({
const paragraphs = editor.getEditorState().toJSON().root.children; listItem,
paragraphs.forEach((paragraph: any) => { listType,
const children = paragraph.children || []; index,
const paragraphText: string[] = []; indentLevel
children.forEach((child: any) => { }: {
if (child.type === 'linebreak') { listItem: ListItemEditorNode;
paragraphText.push('\n'); listType: 'bullet' | 'number';
} else if (child.text) { index: number;
paragraphText.push(child.text); indentLevel: number;
} else if (child.type === 'variableLabel' || child.type === 'Variable') { }) => {
paragraphText.push(child.variableKey); const results = [];
}
const itemText: string[] = [];
const nestedLists: ListEditorNode[] = [];
// Separate text and nested lists
listItem.children.forEach((child) => {
if (child.type === 'linebreak') {
itemText.push('\n');
} else if (child.type === 'text') {
itemText.push(child.text);
} else if (child.type === 'tab') {
itemText.push(' ');
} else if (child.type === 'variableLabel' || child.type === 'Variable') {
itemText.push(child.variableKey);
} else if (child.type === 'list') {
nestedLists.push(child);
}
});
// Add prefix and indent
const itemTextString = itemText.join('').trim();
const indent = ' '.repeat(indentLevel);
const prefix = listType === 'bullet' ? '- ' : `${index + 1}. `;
results.push(indent + prefix + itemTextString);
// Handle nested lists
nestedLists.forEach((nestedList) => {
const nestedResults = processList({
list: nestedList,
indentLevel: indentLevel + 1
}); });
editorStateTextString.push(paragraphText.join('')); results.push(...nestedResults);
});
return results;
};
const processList = ({ list, indentLevel = 0 }: { list: ListEditorNode; indentLevel?: number }) => {
const results: string[] = [];
list.children.forEach((listItem, index: number) => {
if (listItem.type === 'listitem') {
const itemResults = processListItem({
listItem,
listType: list.listType,
index,
indentLevel
});
results.push(...itemResults);
}
});
return results;
};
export const editorStateToText = (editor: LexicalEditor) => {
const editorStateTextString: string[] = [];
const editorState = editor.getEditorState().toJSON() as EditorState;
const paragraphs = editorState.root.children;
paragraphs.forEach((paragraph) => {
if (paragraph.type === 'list') {
const listResults = processList({ list: paragraph });
editorStateTextString.push(...listResults);
} else if (paragraph.type === 'paragraph') {
const children = paragraph.children;
const paragraphText: string[] = [];
const indentSpaces = ' '.repeat(paragraph.indent || 0);
children.forEach((child) => {
if (child.type === 'linebreak') {
paragraphText.push('\n');
} else if (child.type === 'text') {
paragraphText.push(child.text);
} else if (child.type === 'tab') {
paragraphText.push(' ');
} else if (child.type === 'variableLabel' || child.type === 'Variable') {
paragraphText.push(child.variableKey);
}
});
const finalText = paragraphText.join('');
editorStateTextString.push(indentSpaces + finalText);
}
}); });
return editorStateTextString.join('\n'); return editorStateTextString.join('\n');
} };
...@@ -26,6 +26,7 @@ import { ...@@ -26,6 +26,7 @@ import {
import { type PaginationProps, type PaginationResponse } from '../common/fetch/type'; import { type PaginationProps, type PaginationResponse } from '../common/fetch/type';
import MyMenu from '../components/common/MyMenu'; import MyMenu from '../components/common/MyMenu';
import { useSystem } from './useSystem'; import { useSystem } from './useSystem';
import { useRouter } from 'next/router';
const thresholdVal = 200; const thresholdVal = 200;
...@@ -35,19 +36,18 @@ export function usePagination<DataT, ResT = {}>( ...@@ -35,19 +36,18 @@ export function usePagination<DataT, ResT = {}>(
defaultPageSize = 10, defaultPageSize = 10,
pageSizeOptions: defaultPageSizeOptions, pageSizeOptions: defaultPageSizeOptions,
params, params,
defaultRequest = true,
type = 'button', type = 'button',
onChange, onChange,
refreshDeps, refreshDeps,
scrollLoadType = 'bottom', scrollLoadType = 'bottom',
EmptyTip, EmptyTip,
pollingInterval, pollingInterval,
pollingWhenHidden = false pollingWhenHidden = false,
storeToQuery = false
}: { }: {
defaultPageSize?: number; defaultPageSize?: number;
pageSizeOptions?: number[]; pageSizeOptions?: number[];
params?: DataT; params?: DataT;
defaultRequest?: boolean;
type?: 'button' | 'scroll'; type?: 'button' | 'scroll';
onChange?: (pageNum: number) => void; onChange?: (pageNum: number) => void;
refreshDeps?: any[]; refreshDeps?: any[];
...@@ -56,15 +56,20 @@ export function usePagination<DataT, ResT = {}>( ...@@ -56,15 +56,20 @@ export function usePagination<DataT, ResT = {}>(
EmptyTip?: React.JSX.Element; EmptyTip?: React.JSX.Element;
pollingInterval?: number; pollingInterval?: number;
pollingWhenHidden?: boolean; pollingWhenHidden?: boolean;
storeToQuery?: boolean;
} }
) { ) {
const router = useRouter();
let { page = '1' } = router.query as { page: string };
const numPage = Number(page);
const { toast } = useToast(); const { toast } = useToast();
const { isPc } = useSystem(); const { isPc } = useSystem();
const { t } = useTranslation(); const { t } = useTranslation();
const [isLoading, { setTrue, setFalse }] = useBoolean(false); const [isLoading, { setTrue, setFalse }] = useBoolean(false);
const [pageNum, setPageNum] = useState(1); const [pageNum, setPageNum] = useState(numPage);
const [pageSize, setPageSize] = useState(defaultPageSize); const [pageSize, setPageSize] = useState(defaultPageSize);
const pageSizeOptions = useCreation( const pageSizeOptions = useCreation(
() => defaultPageSizeOptions || [10, 20, 50, 100], () => defaultPageSizeOptions || [10, 20, 50, 100],
...@@ -76,7 +81,7 @@ export function usePagination<DataT, ResT = {}>( ...@@ -76,7 +81,7 @@ export function usePagination<DataT, ResT = {}>(
const totalDataLength = useMemo(() => Math.max(total, data.length), [total, data.length]); const totalDataLength = useMemo(() => Math.max(total, data.length), [total, data.length]);
const isEmpty = total === 0 && !isLoading; const isEmpty = total === 0 && !isLoading;
const noMore = data.length >= totalDataLength; const noMore = data.length > 0 && data.length >= totalDataLength;
const fetchData = useMemoizedFn( const fetchData = useMemoizedFn(
async (num: number = pageNum, ScrollContainerRef?: RefObject<HTMLDivElement>) => { async (num: number = pageNum, ScrollContainerRef?: RefObject<HTMLDivElement>) => {
...@@ -92,6 +97,16 @@ export function usePagination<DataT, ResT = {}>( ...@@ -92,6 +97,16 @@ export function usePagination<DataT, ResT = {}>(
}); });
setPageNum(num); setPageNum(num);
if (storeToQuery && num !== pageNum) {
router.replace({
pathname: router.pathname,
query: {
...router.query,
page: num
}
});
}
res.total !== undefined && setTotal(res.total); res.total !== undefined && setTotal(res.total);
if (type === 'scroll') { if (type === 'scroll') {
...@@ -268,7 +283,8 @@ export function usePagination<DataT, ResT = {}>( ...@@ -268,7 +283,8 @@ export function usePagination<DataT, ResT = {}>(
// Watch scroll position // Watch scroll position
useThrottleEffect( useThrottleEffect(
() => { () => {
if (!ref?.current || type !== 'scroll' || noMore || isLoading) return; if (!ref?.current || type !== 'scroll' || noMore || isLoading || data.length === 0)
return;
const { scrollTop, scrollHeight, clientHeight } = ref.current; const { scrollTop, scrollHeight, clientHeight } = ref.current;
if ( if (
...@@ -313,9 +329,16 @@ export function usePagination<DataT, ResT = {}>( ...@@ -313,9 +329,16 @@ export function usePagination<DataT, ResT = {}>(
); );
// Reload data // Reload data
const isFirstLoad = useRef(true);
const { runAsync: refresh } = useRequest( const { runAsync: refresh } = useRequest(
async () => { async () => {
defaultRequest && fetchData(1); if (isFirstLoad.current) {
isFirstLoad.current = false;
fetchData(numPage);
return;
}
fetchData(1);
}, },
{ {
manual: false, manual: false,
...@@ -323,6 +346,7 @@ export function usePagination<DataT, ResT = {}>( ...@@ -323,6 +346,7 @@ export function usePagination<DataT, ResT = {}>(
throttleWait: 100 throttleWait: 100
} }
); );
// Page size refresh
useEffect(() => { useEffect(() => {
data.length > 0 && fetchData(); data.length > 0 && fetchData();
}, [pageSize]); }, [pageSize]);
......
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
"forbid_hint": "After forbidden, this invitation link will become invalid. This action is irreversible. Are you sure you want to deactivate?", "forbid_hint": "After forbidden, this invitation link will become invalid. This action is irreversible. Are you sure you want to deactivate?",
"forbid_success": "Forbid success", "forbid_success": "Forbid success",
"forbidden": "Forbidden", "forbidden": "Forbidden",
"link_forbidden": "Forbidden", "forbidden_tip": "Confirm disabling {{username}}? The member will be marked as 'disabled' and will not be able to log in. Operation data will not be deleted, and resources under the account will be automatically transferred to the team owner.",
"group": "group", "group": "group",
"group_name": "Group name", "group_name": "Group name",
"handle_invitation": "Handle Invitation", "handle_invitation": "Handle Invitation",
...@@ -113,6 +113,7 @@ ...@@ -113,6 +113,7 @@
"label_sync": "Tag sync", "label_sync": "Tag sync",
"leave": "Resigned", "leave": "Resigned",
"leave_team_failed": "Leaving the team exception", "leave_team_failed": "Leaving the team exception",
"link_forbidden": "Forbidden",
"log_admin_add_plan": "【{{name}}】A package will be added to a team with a team id [{{teamId}}]", "log_admin_add_plan": "【{{name}}】A package will be added to a team with a team id [{{teamId}}]",
"log_admin_add_user": "【{{name}}】Create a user named [{{userName}}]", "log_admin_add_user": "【{{name}}】Create a user named [{{userName}}]",
"log_admin_change_license": "【{{name}}】Changed License", "log_admin_change_license": "【{{name}}】Changed License",
...@@ -196,6 +197,7 @@ ...@@ -196,6 +197,7 @@
"log_user": "Operator", "log_user": "Operator",
"login": "Log in", "login": "Log in",
"manage_member": "Managing members", "manage_member": "Managing members",
"manage_per": "Administrative permissions",
"member": "member", "member": "member",
"member_group": "Belonging to member group", "member_group": "Belonging to member group",
"move_app": "App location movement", "move_app": "App location movement",
...@@ -222,7 +224,6 @@ ...@@ -222,7 +224,6 @@
"relocate_department": "Department Mobile", "relocate_department": "Department Mobile",
"remark": "remark", "remark": "remark",
"remove_tip": "Confirm to remove {{username}} from the team? The member will be marked as 'leave'. Operation data will not be deleted, and resources under the account will be automatically transferred to the team owner.", "remove_tip": "Confirm to remove {{username}} from the team? The member will be marked as 'leave'. Operation data will not be deleted, and resources under the account will be automatically transferred to the team owner.",
"forbidden_tip": "Confirm disabling {{username}}? The member will be marked as 'disabled' and will not be able to log in. Operation data will not be deleted, and resources under the account will be automatically transferred to the team owner.",
"restore_tip": "Confirm to join the team {{username}}? \nOnly the availability and related permissions of this member account are restored, and the resources under the account cannot be restored.", "restore_tip": "Confirm to join the team {{username}}? \nOnly the availability and related permissions of this member account are restored, and the resources under the account cannot be restored.",
"restore_tip_title": "Recovery confirmation", "restore_tip_title": "Recovery confirmation",
"retain_admin_permissions": "Keep administrator rights", "retain_admin_permissions": "Keep administrator rights",
......
...@@ -176,7 +176,7 @@ ...@@ -176,7 +176,7 @@
"module.type": "\"{{type}}\" type\n{{description}}", "module.type": "\"{{type}}\" type\n{{description}}",
"modules.Title is required": "Module name cannot be empty", "modules.Title is required": "Module name cannot be empty",
"month.unit": "Day", "month.unit": "Day",
"move.hint": "After moving, the selected application/folder will inherit the permission settings of the new folder, and the original permission settings will become invalid.", "move.hint": "After moving, the selected app/folder will inherit the permission settings for the new folder.",
"move_app": "Move Application", "move_app": "Move Application",
"no_mcp_tools_list": "No data yet, the MCP address needs to be parsed first", "no_mcp_tools_list": "No data yet, the MCP address needs to be parsed first",
"node_not_intro": "This node is not introduced", "node_not_intro": "This node is not introduced",
...@@ -190,7 +190,7 @@ ...@@ -190,7 +190,7 @@
"pdf_enhance_parse": "PDF enhancement analysis", "pdf_enhance_parse": "PDF enhancement analysis",
"pdf_enhance_parse_price": "{{price}}Points/page", "pdf_enhance_parse_price": "{{price}}Points/page",
"pdf_enhance_parse_tips": "Calling PDF recognition model for parsing, you can convert it into Markdown and retain pictures in the document. At the same time, you can also identify scanned documents, which will take a long time to identify them.", "pdf_enhance_parse_tips": "Calling PDF recognition model for parsing, you can convert it into Markdown and retain pictures in the document. At the same time, you can also identify scanned documents, which will take a long time to identify them.",
"permission.des.manage": "Based on write permissions, you can configure publishing channels, view conversation logs, and assign permissions to the application.", "permission.des.manage": "Can configure publishing channels, view logs, and assign application permissions",
"permission.des.read": "Use the app to have conversations", "permission.des.read": "Use the app to have conversations",
"permission.des.readChatLog": "Can view chat logs", "permission.des.readChatLog": "Can view chat logs",
"permission.des.write": "Can view and edit apps", "permission.des.write": "Can view and edit apps",
......
...@@ -966,7 +966,7 @@ ...@@ -966,7 +966,7 @@
"permission.Private Tip": "Only Available to Yourself", "permission.Private Tip": "Only Available to Yourself",
"permission.Public": "Team", "permission.Public": "Team",
"permission.Public Tip": "Available to All Team Members", "permission.Public Tip": "Available to All Team Members",
"permission.Remove InheritPermission Confirm": "This operation will invalidate permission inheritance. Proceed?", "permission.Remove InheritPermission Confirm": "This modification conflicts with inheritance permissions, which will cause permission inheritance to be invalid. Will it be carried out?",
"permission.Resume InheritPermission Confirm": "Resume inheriting permissions from the parent folder?", "permission.Resume InheritPermission Confirm": "Resume inheriting permissions from the parent folder?",
"permission.Resume InheritPermission Failed": "Resume Failed", "permission.Resume InheritPermission Failed": "Resume Failed",
"permission.Resume InheritPermission Success": "Resume Successful", "permission.Resume InheritPermission Success": "Resume Successful",
...@@ -976,6 +976,7 @@ ...@@ -976,6 +976,7 @@
"permission.change_owner_success": "Ownership Transferred Successfully", "permission.change_owner_success": "Ownership Transferred Successfully",
"permission.change_owner_tip": "Your permissions will not be retained after the transfer", "permission.change_owner_tip": "Your permissions will not be retained after the transfer",
"permission.change_owner_to": "Transfer to", "permission.change_owner_to": "Transfer to",
"permission.common_member": "Common members",
"permission.manager": "administrator", "permission.manager": "administrator",
"permission.read": "Read permission", "permission.read": "Read permission",
"permission.write": "write permission", "permission.write": "write permission",
......
...@@ -133,7 +133,7 @@ ...@@ -133,7 +133,7 @@
"llm_paragraph_mode_force_desc": "Force the use of the model to automatically identify paragraphs and ignore paragraphs in the original text (if any)", "llm_paragraph_mode_force_desc": "Force the use of the model to automatically identify paragraphs and ignore paragraphs in the original text (if any)",
"loading": "Loading...", "loading": "Loading...",
"max_chunk_size": "Maximum chunk size", "max_chunk_size": "Maximum chunk size",
"move.hint": "After moving, the selected knowledge base/folder will inherit the permission settings of the new folder, and the original permission settings will become invalid.", "move.hint": "After moving, the selected knowledge base/folder will inherit the permission settings for the new folder.",
"noChildren": "No subdirectories", "noChildren": "No subdirectories",
"noSelectedFolder": "No selected folder", "noSelectedFolder": "No selected folder",
"noSelectedId": "No selected ID", "noSelectedId": "No selected ID",
......
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
"delete.admin_success": "Admin Deleted Successfully", "delete.admin_success": "Admin Deleted Successfully",
"delete.failed": "Delete failed", "delete.failed": "Delete failed",
"delete.success": "Delete successfully", "delete.success": "Delete successfully",
"has_chosen": "Selected",
"login.Dingtalk": "DingTalk Login", "login.Dingtalk": "DingTalk Login",
"login.error": "Login Error", "login.error": "Login Error",
"login.password_condition": "Password can be up to 60 characters", "login.password_condition": "Password can be up to 60 characters",
......
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
"forbid_hint": "停用后,该邀请链接将失效。 该操作不可撤销,是否确认停用?", "forbid_hint": "停用后,该邀请链接将失效。 该操作不可撤销,是否确认停用?",
"forbid_success": "停用成功", "forbid_success": "停用成功",
"forbidden": "停用", "forbidden": "停用",
"link_forbidden": "禁用", "forbidden_tip": "确认将 {{username}} 禁用?成员将被标记为“禁用”并无法登录,不删除操作数据,账号下资源自动转让给团队所有者。",
"group": "群组", "group": "群组",
"group_name": "群组名称", "group_name": "群组名称",
"handle_invitation": "处理团队邀请", "handle_invitation": "处理团队邀请",
...@@ -115,6 +115,7 @@ ...@@ -115,6 +115,7 @@
"label_sync": "标签同步", "label_sync": "标签同步",
"leave": "离开", "leave": "离开",
"leave_team_failed": "离开团队异常", "leave_team_failed": "离开团队异常",
"link_forbidden": "禁用",
"log_admin_add_plan": "【{{name}}】将给团队id为【{{teamId}}】的团队添加了套餐", "log_admin_add_plan": "【{{name}}】将给团队id为【{{teamId}}】的团队添加了套餐",
"log_admin_add_user": "【{{name}}】创建了一个名为【{{userName}}】的用户", "log_admin_add_user": "【{{name}}】创建了一个名为【{{userName}}】的用户",
"log_admin_change_license": "【{{name}}】变更了License", "log_admin_change_license": "【{{name}}】变更了License",
...@@ -200,6 +201,7 @@ ...@@ -200,6 +201,7 @@
"log_user": "操作人员", "log_user": "操作人员",
"login": "登录", "login": "登录",
"manage_member": "管理成员", "manage_member": "管理成员",
"manage_per": "管理权限",
"member": "成员", "member": "成员",
"member_group": "所属群组", "member_group": "所属群组",
"move_app": "应用位置移动", "move_app": "应用位置移动",
...@@ -226,7 +228,6 @@ ...@@ -226,7 +228,6 @@
"relocate_department": "部门移动", "relocate_department": "部门移动",
"remark": "备注", "remark": "备注",
"remove_tip": "确认将 {{username}} 移出团队?成员将被标记为“离开”,不删除操作数据,账号下资源自动转让给团队所有者。", "remove_tip": "确认将 {{username}} 移出团队?成员将被标记为“离开”,不删除操作数据,账号下资源自动转让给团队所有者。",
"forbidden_tip": "确认将 {{username}} 禁用?成员将被标记为“禁用”并无法登录,不删除操作数据,账号下资源自动转让给团队所有者。",
"restore_tip": "确认将 {{username}} 加入团队吗?仅恢复该成员账号可用性及相关权限,无法恢复账号下资源。", "restore_tip": "确认将 {{username}} 加入团队吗?仅恢复该成员账号可用性及相关权限,无法恢复账号下资源。",
"restore_tip_title": "恢复确认", "restore_tip_title": "恢复确认",
"retain_admin_permissions": "保留管理员权限", "retain_admin_permissions": "保留管理员权限",
......
...@@ -185,7 +185,7 @@ ...@@ -185,7 +185,7 @@
"module.type": "\"{{type}}\"类型\n{{description}}", "module.type": "\"{{type}}\"类型\n{{description}}",
"modules.Title is required": "模块名不能为空", "modules.Title is required": "模块名不能为空",
"month.unit": "号", "month.unit": "号",
"move.hint": "移动后,所选应用/文件夹将继承新文件夹的权限设置,原先的权限设置失效。", "move.hint": "移动后,所选应用/文件夹将继承新文件夹的权限设置。",
"move_app": "移动应用", "move_app": "移动应用",
"no_mcp_tools_list": "暂无数据,需先解析 MCP 地址", "no_mcp_tools_list": "暂无数据,需先解析 MCP 地址",
"node_not_intro": "这个节点没有介绍", "node_not_intro": "这个节点没有介绍",
...@@ -199,7 +199,7 @@ ...@@ -199,7 +199,7 @@
"pdf_enhance_parse": "PDF增强解析", "pdf_enhance_parse": "PDF增强解析",
"pdf_enhance_parse_price": "{{price}}积分/页", "pdf_enhance_parse_price": "{{price}}积分/页",
"pdf_enhance_parse_tips": "调用 PDF 识别模型进行解析,可以将其转换成 Markdown 并保留文档中的图片,同时也可以对扫描件进行识别,识别时间较长。", "pdf_enhance_parse_tips": "调用 PDF 识别模型进行解析,可以将其转换成 Markdown 并保留文档中的图片,同时也可以对扫描件进行识别,识别时间较长。",
"permission.des.manage": "写权限基础上,可配置发布渠道、查看对话日志、分配该应用权限", "permission.des.manage": "可配置发布渠道、查看日志、分配应用权限",
"permission.des.read": "可使用该应用进行对话", "permission.des.read": "可使用该应用进行对话",
"permission.des.readChatLog": "可查看对话日志", "permission.des.readChatLog": "可查看对话日志",
"permission.des.write": "可查看和编辑应用", "permission.des.write": "可查看和编辑应用",
......
...@@ -967,7 +967,7 @@ ...@@ -967,7 +967,7 @@
"permission.Private Tip": "仅自己可用", "permission.Private Tip": "仅自己可用",
"permission.Public": "协作", "permission.Public": "协作",
"permission.Public Tip": "团队所有成员可使用", "permission.Public Tip": "团队所有成员可使用",
"permission.Remove InheritPermission Confirm": "此操作会导致权限继承失效,是否进行?", "permission.Remove InheritPermission Confirm": "此修改与继承权限存在冲突,会导致权限继承失效,是否进行?",
"permission.Resume InheritPermission Confirm": "是否恢复为继承父级文件夹的权限?", "permission.Resume InheritPermission Confirm": "是否恢复为继承父级文件夹的权限?",
"permission.Resume InheritPermission Failed": "恢复失败", "permission.Resume InheritPermission Failed": "恢复失败",
"permission.Resume InheritPermission Success": "恢复成功", "permission.Resume InheritPermission Success": "恢复成功",
...@@ -977,6 +977,7 @@ ...@@ -977,6 +977,7 @@
"permission.change_owner_success": "成功转移所有权", "permission.change_owner_success": "成功转移所有权",
"permission.change_owner_tip": "转移后您的权限不会保留", "permission.change_owner_tip": "转移后您的权限不会保留",
"permission.change_owner_to": "转移给", "permission.change_owner_to": "转移给",
"permission.common_member": "普通成员",
"permission.manager": "管理员", "permission.manager": "管理员",
"permission.read": "读权限", "permission.read": "读权限",
"permission.write": "写权限", "permission.write": "写权限",
......
...@@ -133,7 +133,7 @@ ...@@ -133,7 +133,7 @@
"llm_paragraph_mode_force_desc": "强制使用模型自动识别段落,并忽略原文本的段落(如有)", "llm_paragraph_mode_force_desc": "强制使用模型自动识别段落,并忽略原文本的段落(如有)",
"loading": "加载中...", "loading": "加载中...",
"max_chunk_size": "最大分块大小", "max_chunk_size": "最大分块大小",
"move.hint": "移动后,所选知识库/文件夹将继承新文件夹的权限设置,原先的权限设置失效。", "move.hint": "移动后,所选知识库/文件夹将继承新文件夹的权限设置。",
"noChildren": "无子目录", "noChildren": "无子目录",
"noSelectedFolder": "没有选择文件夹", "noSelectedFolder": "没有选择文件夹",
"noSelectedId": "没有选择 ID", "noSelectedId": "没有选择 ID",
......
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
"delete.admin_success": "删除管理员成功", "delete.admin_success": "删除管理员成功",
"delete.failed": "删除失败", "delete.failed": "删除失败",
"delete.success": "删除成功", "delete.success": "删除成功",
"has_chosen": "已选择",
"login.Dingtalk": "钉钉登录", "login.Dingtalk": "钉钉登录",
"login.error": "登录异常", "login.error": "登录异常",
"login.password_condition": "密码最多 60 位", "login.password_condition": "密码最多 60 位",
......
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
"forbid_hint": "停用後,該邀請連結將失效。該操作不可撤銷,是否確認停用?", "forbid_hint": "停用後,該邀請連結將失效。該操作不可撤銷,是否確認停用?",
"forbid_success": "停用成功", "forbid_success": "停用成功",
"forbidden": "停用", "forbidden": "停用",
"link_forbidden": "禁用", "forbidden_tip": "確認將 {{username}} 禁用?成員將被標記為“禁用”並無法登錄,不刪除操作數據,賬號下資源自動轉讓給團隊所有者。",
"group": "群組", "group": "群組",
"group_name": "群組名稱", "group_name": "群組名稱",
"handle_invitation": "處理團隊邀請", "handle_invitation": "處理團隊邀請",
...@@ -113,6 +113,7 @@ ...@@ -113,6 +113,7 @@
"label_sync": "標籤同步", "label_sync": "標籤同步",
"leave": "已離職", "leave": "已離職",
"leave_team_failed": "離開團隊異常", "leave_team_failed": "離開團隊異常",
"link_forbidden": "禁用",
"log_admin_add_plan": "【{{name}}】將給團隊id為【{{teamId}}】的團隊添加了套餐", "log_admin_add_plan": "【{{name}}】將給團隊id為【{{teamId}}】的團隊添加了套餐",
"log_admin_add_user": "【{{name}}】創建了一個名為【{{userName}}】的用戶", "log_admin_add_user": "【{{name}}】創建了一個名為【{{userName}}】的用戶",
"log_admin_change_license": "【{{name}}】變更了License", "log_admin_change_license": "【{{name}}】變更了License",
...@@ -196,6 +197,7 @@ ...@@ -196,6 +197,7 @@
"log_user": "操作人員", "log_user": "操作人員",
"login": "登入", "login": "登入",
"manage_member": "管理成員", "manage_member": "管理成員",
"manage_per": "管理權限",
"member": "成員", "member": "成員",
"member_group": "所屬成員組", "member_group": "所屬成員組",
"move_app": "應用位置移動", "move_app": "應用位置移動",
...@@ -222,7 +224,6 @@ ...@@ -222,7 +224,6 @@
"relocate_department": "部門移動", "relocate_department": "部門移動",
"remark": "備註", "remark": "備註",
"remove_tip": "確認將 {{username}} 移出團隊?成員將被標記為“離開”,不刪除操作數據,賬號下資源自動轉讓給團隊所有者。", "remove_tip": "確認將 {{username}} 移出團隊?成員將被標記為“離開”,不刪除操作數據,賬號下資源自動轉讓給團隊所有者。",
"forbidden_tip": "確認將 {{username}} 禁用?成員將被標記為“禁用”並無法登錄,不刪除操作數據,賬號下資源自動轉讓給團隊所有者。",
"restore_tip": "確認將 {{username}} 加入團隊嗎?\n僅恢復該成員賬號可用性及相關權限,無法恢復賬號下資源。", "restore_tip": "確認將 {{username}} 加入團隊嗎?\n僅恢復該成員賬號可用性及相關權限,無法恢復賬號下資源。",
"restore_tip_title": "恢復確認", "restore_tip_title": "恢復確認",
"retain_admin_permissions": "保留管理員權限", "retain_admin_permissions": "保留管理員權限",
......
...@@ -175,7 +175,7 @@ ...@@ -175,7 +175,7 @@
"module.type": "\"{{type}}\" 類型\n{{description}}", "module.type": "\"{{type}}\" 類型\n{{description}}",
"modules.Title is required": "模組名稱不能空白", "modules.Title is required": "模組名稱不能空白",
"month.unit": "號", "month.unit": "號",
"move.hint": "移動後,所選應用程式/資料夾將會繼承新資料夾的權限設定,原先的權限設定將會失效。", "move.hint": "移動後,所選應用/文件夾將繼承新文件夾的權限設置。",
"move_app": "移動應用程式", "move_app": "移動應用程式",
"no_mcp_tools_list": "暫無數據,需先解析 MCP 地址", "no_mcp_tools_list": "暫無數據,需先解析 MCP 地址",
"node_not_intro": "這個節點沒有介紹", "node_not_intro": "這個節點沒有介紹",
...@@ -189,7 +189,7 @@ ...@@ -189,7 +189,7 @@
"pdf_enhance_parse": "PDF 增強解析", "pdf_enhance_parse": "PDF 增強解析",
"pdf_enhance_parse_price": "{{price}}積分/頁", "pdf_enhance_parse_price": "{{price}}積分/頁",
"pdf_enhance_parse_tips": "呼叫 PDF 識別模型進行解析,可以將其轉換成 Markdown 並保留文件中的圖片,同時也可以對掃描件進行識別,識別時間較長。", "pdf_enhance_parse_tips": "呼叫 PDF 識別模型進行解析,可以將其轉換成 Markdown 並保留文件中的圖片,同時也可以對掃描件進行識別,識別時間較長。",
"permission.des.manage": "在寫入權限基礎上,可以設定發布通道、檢視對話紀錄、分配這個應用程式的權限", "permission.des.manage": "可配置發布渠道、查看日誌、分配應用權限",
"permission.des.read": "可以使用這個應用程式進行對話", "permission.des.read": "可以使用這個應用程式進行對話",
"permission.des.readChatLog": "可以檢視對話紀錄", "permission.des.readChatLog": "可以檢視對話紀錄",
"permission.des.write": "可以檢視和編輯應用程式", "permission.des.write": "可以檢視和編輯應用程式",
......
...@@ -965,7 +965,7 @@ ...@@ -965,7 +965,7 @@
"permission.Private Tip": "僅自己可用", "permission.Private Tip": "僅自己可用",
"permission.Public": "團隊", "permission.Public": "團隊",
"permission.Public Tip": "所有團隊成員可用", "permission.Public Tip": "所有團隊成員可用",
"permission.Remove InheritPermission Confirm": "此操作會導致權限繼承失效,是否繼續?", "permission.Remove InheritPermission Confirm": "此修改與繼承權限存在衝突,會導致權限繼承失效,是否進行?",
"permission.Resume InheritPermission Confirm": "要恢復繼承上層資料夾的權限嗎?", "permission.Resume InheritPermission Confirm": "要恢復繼承上層資料夾的權限嗎?",
"permission.Resume InheritPermission Failed": "恢復失敗", "permission.Resume InheritPermission Failed": "恢復失敗",
"permission.Resume InheritPermission Success": "恢復成功", "permission.Resume InheritPermission Success": "恢復成功",
...@@ -975,6 +975,7 @@ ...@@ -975,6 +975,7 @@
"permission.change_owner_success": "擁有權轉移成功", "permission.change_owner_success": "擁有權轉移成功",
"permission.change_owner_tip": "轉移後您的權限將不會保留", "permission.change_owner_tip": "轉移後您的權限將不會保留",
"permission.change_owner_to": "轉移給", "permission.change_owner_to": "轉移給",
"permission.common_member": "普通成員",
"permission.manager": "管理員", "permission.manager": "管理員",
"permission.read": "讀取權限", "permission.read": "讀取權限",
"permission.write": "寫入權限", "permission.write": "寫入權限",
......
...@@ -133,7 +133,7 @@ ...@@ -133,7 +133,7 @@
"llm_paragraph_mode_force_desc": "強制使用模型自動識別段落,並忽略原文本的段落(如有)", "llm_paragraph_mode_force_desc": "強制使用模型自動識別段落,並忽略原文本的段落(如有)",
"loading": "加載中...", "loading": "加載中...",
"max_chunk_size": "最大分塊大小", "max_chunk_size": "最大分塊大小",
"move.hint": "移動後,所選資料集/資料夾將繼承新資料夾的權限設定,原先的權限設定將失效。", "move.hint": "移動後,所選知識庫/文件夾將繼承新文件夾的權限設置。",
"noChildren": "無子目錄", "noChildren": "無子目錄",
"noSelectedFolder": "沒有選擇文件夾", "noSelectedFolder": "沒有選擇文件夾",
"noSelectedId": "沒有選擇 ID", "noSelectedId": "沒有選擇 ID",
......
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
"delete.admin_success": "刪除管理員成功", "delete.admin_success": "刪除管理員成功",
"delete.failed": "刪除失敗", "delete.failed": "刪除失敗",
"delete.success": "刪除成功", "delete.success": "刪除成功",
"has_chosen": "已選擇",
"login.Dingtalk": "釘釘登入", "login.Dingtalk": "釘釘登入",
"login.error": "登入失敗", "login.error": "登入失敗",
"login.password_condition": "密碼最多可輸入 60 個字元", "login.password_condition": "密碼最多可輸入 60 個字元",
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
"ahooks": "^3.9.4", "ahooks": "^3.9.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"next": "14.2.32",
"i18next": "23.16.8", "i18next": "23.16.8",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lexical": "0.12.6", "lexical": "0.12.6",
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
"dependencies": { "dependencies": {
"@types/node-fetch": "^2.6.12", "@types/node-fetch": "^2.6.12",
"assert": "^2.1.0", "assert": "^2.1.0",
"axios": "^1.8.2", "axios": "^1.12.1",
"body-parser": "^1.20.3", "body-parser": "^1.20.3",
"browserify-zlib": "^0.2.0", "browserify-zlib": "^0.2.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
"dependencies": { "dependencies": {
"@types/node-fetch": "^2.6.12", "@types/node-fetch": "^2.6.12",
"assert": "^2.1.0", "assert": "^2.1.0",
"axios": "^1.8.2", "axios": "^1.12.1",
"body-parser": "^1.20.3", "body-parser": "^1.20.3",
"browserify-zlib": "^0.2.0", "browserify-zlib": "^0.2.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
"@node-rs/jieba": "2.0.1", "@node-rs/jieba": "2.0.1",
"@tanstack/react-query": "^4.24.10", "@tanstack/react-query": "^4.24.10",
"ahooks": "^3.7.11", "ahooks": "^3.7.11",
"axios": "^1.8.2", "axios": "^1.12.1",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"echarts": "5.4.1", "echarts": "5.4.1",
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mermaid": "^10.9.4", "mermaid": "^10.9.4",
"nanoid": "^5.1.3", "nanoid": "^5.1.3",
"next": "14.2.28", "next": "14.2.32",
"next-i18next": "15.4.2", "next-i18next": "15.4.2",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
......
import { useEffect, useRef } from 'react'; import { useEffect } from 'react';
import { getCouponCode, removeCouponCode } from '@/web/support/marketing/utils'; import { getCouponCode, removeCouponCode } from '@/web/support/marketing/utils';
import type { UserType } from '@fastgpt/global/support/user/type.d';
import { redeemCoupon } from '@/web/support/user/team/api'; import { redeemCoupon } from '@/web/support/user/team/api';
import { useUserStore } from '@/web/support/user/useUserStore';
export const useCheckCoupon = (userInfo: UserType | null) => { export const useCheckCoupon = () => {
const hasCheckedCouponRef = useRef(false); const { userInfo } = useUserStore();
useEffect(() => { useEffect(() => {
if (!userInfo || hasCheckedCouponRef.current) return; if (!userInfo) return;
const couponCode = getCouponCode(); const couponCode = getCouponCode();
if (!couponCode) return; if (!couponCode) return;
hasCheckedCouponRef.current = true;
redeemCoupon(couponCode) redeemCoupon(couponCode)
.catch(() => {}) .then(removeCouponCode)
.finally(removeCouponCode); .catch((err) => {
if (err?.message === 'Invalid coupon') {
removeCouponCode();
}
});
}, [userInfo]); }, [userInfo]);
}; };
......
...@@ -75,7 +75,7 @@ const Layout = ({ children }: { children: JSX.Element }) => { ...@@ -75,7 +75,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
const { userInfo, isUpdateNotification, setIsUpdateNotification } = useUserStore(); const { userInfo, isUpdateNotification, setIsUpdateNotification } = useUserStore();
const { setUserDefaultLng } = useI18nLng(); const { setUserDefaultLng } = useI18nLng();
useCheckCoupon(userInfo); useCheckCoupon();
const isChatPage = useMemo( const isChatPage = useMemo(
() => router.pathname === '/chat' && Object.values(router.query).join('').length !== 0, () => router.pathname === '/chat' && Object.values(router.query).join('').length !== 0,
......
...@@ -137,7 +137,7 @@ const FolderSlideCard = ({ ...@@ -137,7 +137,7 @@ const FolderSlideCard = ({
isInheritPermission={isInheritPermission} isInheritPermission={isInheritPermission}
hasParent={hasParent} hasParent={hasParent}
> >
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { {({ MemberListCard, onOpenManageModal }) => {
return ( return (
<> <>
<Flex alignItems="center" justifyContent="space-between"> <Flex alignItems="center" justifyContent="space-between">
...@@ -145,26 +145,15 @@ const FolderSlideCard = ({ ...@@ -145,26 +145,15 @@ const FolderSlideCard = ({
{t('common:permission.Collaborator')} {t('common:permission.Collaborator')}
</Box> </Box>
{managePer.permission.hasManagePer && ( {managePer.permission.hasManagePer && (
<HStack spacing={3}> <MyTooltip label={t('common:permission.Manage')}>
<MyTooltip label={t('common:permission.Manage')}> <MyIcon
<MyIcon w="1rem"
w="1rem" name="common/settingLight"
name="common/settingLight" cursor={'pointer'}
cursor={'pointer'} _hover={{ color: 'primary.600' }}
_hover={{ color: 'primary.600' }} onClick={onOpenManageModal}
onClick={onOpenManageModal} />
/> </MyTooltip>
</MyTooltip>
<MyTooltip label={t('common:Add')}>
<MyIcon
w="1rem"
name="support/permission/collaborator"
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
onClick={onOpenAddMember}
/>
</MyTooltip>
</HStack>
)} )}
</Flex> </Flex>
<MemberListCard <MemberListCard
......
...@@ -124,19 +124,21 @@ const VariableInput = ({ ...@@ -124,19 +124,21 @@ const VariableInput = ({
bg={'white'} bg={'white'}
boxShadow={'0 0 8px rgba(0,0,0,0.15)'} boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
> >
<Flex {chatType !== ChatTypeEnum.chat && (
color={'primary.600'} <Flex
bg={'primary.100'} color={'primary.600'}
mb={3} bg={'primary.100'}
px={3} mb={3}
py={1.5} px={3}
gap={1} py={1.5}
fontSize={'mini'} gap={1}
rounded={'sm'} fontSize={'mini'}
> rounded={'sm'}
<MyIcon name={'common/info'} color={'primary.600'} w={4} /> >
{t('chat:variable_invisable_in_share')} <MyIcon name={'common/info'} color={'primary.600'} w={4} />
</Flex> {t('chat:variable_invisable_in_share')}
</Flex>
)}
{externalVariableList.map((item) => { {externalVariableList.map((item) => {
return ( return (
<LabelAndFormRender <LabelAndFormRender
......
...@@ -33,19 +33,6 @@ const ChatHomeVariablesForm = ({ chatForm }: Props) => { ...@@ -33,19 +33,6 @@ const ChatHomeVariablesForm = ({ chatForm }: Props) => {
{/* custom variables */} {/* custom variables */}
{allVariableList.filter((i) => i.type === VariableInputEnum.custom).length > 0 && ( {allVariableList.filter((i) => i.type === VariableInputEnum.custom).length > 0 && (
<> <>
<Flex
color={'primary.600'}
bg={'primary.100'}
mb={3}
px={3}
py={1.5}
gap={1}
fontSize={'mini'}
rounded={'sm'}
>
<MyIcon name={'common/info'} color={'primary.600'} w={4} />
{t('chat:variable_invisable_in_share')}
</Flex>
{allVariableList {allVariableList
.filter((i) => i.type === VariableInputEnum.custom) .filter((i) => i.type === VariableInputEnum.custom)
.map((item) => ( .map((item) => (
......
...@@ -1126,7 +1126,8 @@ const ChatBox = ({ ...@@ -1126,7 +1126,8 @@ const ChatBox = ({
> >
<Flex h={'100%'} flexDir={'column'} justifyContent={'center'} w={'100%'}> <Flex h={'100%'} flexDir={'column'} justifyContent={'center'} w={'100%'}>
{HomeChatRenderBox} {HomeChatRenderBox}
{allVariableList.length > 0 ? ( {allVariableList.filter((item) => item.type !== VariableInputEnum.internal).length >
0 ? (
<Box w={'100%'}> <Box w={'100%'}>
<ChatHomeVariablesForm chatForm={chatForm} /> <ChatHomeVariablesForm chatForm={chatForm} />
</Box> </Box>
......
...@@ -118,19 +118,21 @@ const VariablePopover = ({ chatType }: { chatType: ChatTypeEnum }) => { ...@@ -118,19 +118,21 @@ const VariablePopover = ({ chatType }: { chatType: ChatTypeEnum }) => {
{externalVariableList.length > 0 && ( {externalVariableList.length > 0 && (
<Box textAlign={'left'}> <Box textAlign={'left'}>
<Flex {chatType !== ChatTypeEnum.chat && (
color={'primary.600'} <Flex
bg={'primary.100'} color={'primary.600'}
mb={3} bg={'primary.100'}
px={3} mb={3}
py={1.5} px={3}
gap={1} py={1.5}
fontSize={'mini'} gap={1}
rounded={'sm'} fontSize={'mini'}
> rounded={'sm'}
<MyIcon name={'common/info'} color={'primary.600'} w={4} /> >
{t('chat:variable_invisable_in_share')} <MyIcon name={'common/info'} color={'primary.600'} w={4} />
</Flex> {t('chat:variable_invisable_in_share')}
</Flex>
)}
{externalVariableList.map((item) => ( {externalVariableList.map((item) => (
<LabelAndFormRender <LabelAndFormRender
{...item} {...item}
......
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
import InputRender from '@/components/core/app/formRender'; import InputRender from '@/components/core/app/formRender';
import { nodeInputTypeToInputType } from '@/components/core/app/formRender/utils'; import { nodeInputTypeToInputType } from '@/components/core/app/formRender/utils';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import LeftRadio from '@fastgpt/web/components/common/Radio/LeftRadio';
const DescriptionBox = React.memo(function DescriptionBox({ const DescriptionBox = React.memo(function DescriptionBox({
description description
...@@ -38,33 +39,26 @@ export const SelectOptionsComponent = React.memo(function SelectOptionsComponent ...@@ -38,33 +39,26 @@ export const SelectOptionsComponent = React.memo(function SelectOptionsComponent
return ( return (
<Box maxW={'100%'}> <Box maxW={'100%'}>
<DescriptionBox description={description} /> <DescriptionBox description={description} />
<Flex flexDirection={'column'} gap={3} w={'250px'}> <Box w={'250px'}>
{userSelectOptions.map((option: UserSelectOptionItemType) => { <LeftRadio<string>
const selected = option.value === userSelectedVal; py={3.5}
gridGap={3}
return ( align={'center'}
<Button list={userSelectOptions.map((option: UserSelectOptionItemType) => ({
key={option.key} title: (
variant={'whitePrimary'} <Box fontSize={'sm'} whiteSpace={'pre-wrap'} wordBreak={'break-word'}>
whiteSpace={'pre-wrap'} {option.value}
isDisabled={!!userSelectedVal} </Box>
{...(selected ),
? { value: option.value
_disabled: { }))}
cursor: 'default', value={userSelectedVal || ''}
borderColor: 'primary.300', defaultBg={'white'}
bg: 'primary.50 !important', activeBg={'white'}
color: 'primary.600' onChange={(val) => onSelect(val)}
} isDisabled={!!userSelectedVal}
} />
: {})} </Box>
onClick={() => onSelect(option.value)}
>
{option.value}
</Button>
);
})}
</Flex>
</Box> </Box>
); );
}); });
......
...@@ -67,7 +67,7 @@ const ConfigPerModal = ({ ...@@ -67,7 +67,7 @@ const ConfigPerModal = ({
isInheritPermission={isInheritPermission} isInheritPermission={isInheritPermission}
hasParent={hasParent} hasParent={hasParent}
> >
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { {({ MemberListCard, onOpenManageModal }) => {
return ( return (
<> <>
<Flex <Flex
...@@ -77,24 +77,14 @@ const ConfigPerModal = ({ ...@@ -77,24 +77,14 @@ const ConfigPerModal = ({
w="full" w="full"
> >
<Box fontSize={'sm'}>{t('common:permission.Collaborator')}</Box> <Box fontSize={'sm'}>{t('common:permission.Collaborator')}</Box>
<Flex flexDirection="row" gap="2"> <Button
<Button size="sm"
size="sm" variant="whitePrimary"
variant="whitePrimary" leftIcon={<MyIcon w="4" name="common/settingLight" />}
leftIcon={<MyIcon w="4" name="common/settingLight" />} onClick={onOpenManageModal}
onClick={onOpenManageModal} >
> {t('common:permission.Manage')}
{t('common:permission.Manage')} </Button>
</Button>
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="support/permission/collaborator" />}
onClick={onOpenAddMember}
>
{t('common:Add')}
</Button>
</Flex>
</Flex> </Flex>
<MemberListCard mt={2} p={1.5} bg="myGray.100" borderRadius="md" /> <MemberListCard mt={2} p={1.5} bg="myGray.100" borderRadius="md" />
</> </>
......
import { useUserStore } from '@/web/support/user/useUserStore';
import { Flex, ModalBody, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Loading from '@fastgpt/web/components/common/MyLoading';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import RoleSelect from './RoleSelect';
import RoleTags from './RoleTags';
import { CollaboratorContext } from './context';
export type ManageModalProps = {
onClose: () => void;
};
function ManageModal({ onClose }: ManageModalProps) {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { permission, collaboratorList, onUpdateCollaborators, onDelOneCollaborator } =
useContextSelector(CollaboratorContext, (v) => v);
const { runAsync: onDelete, loading: isDeleting } = useRequest2(onDelOneCollaborator);
const { runAsync: onUpdate, loading: isUpdating } = useRequest2(onUpdateCollaborators, {
successToast: t('common:update_success'),
errorToast: 'Error'
});
const loading = isDeleting || isUpdating;
return (
<MyModal
isOpen
onClose={onClose}
minW="600px"
title={t('user:team.manage_collaborators')}
iconSrc="common/settingLight"
>
<ModalBody>
<TableContainer borderRadius="md" minH="400px">
<Table>
<Thead bg="myGray.100">
<Tr>
<Th border="none">{t('user:name')}</Th>
<Th border="none">{t('user:permissions')}</Th>
<Th border="none" w={'40px'}>
{t('user:operations')}
</Th>
</Tr>
</Thead>
<Tbody>
<Tr h={'10px'} />
{collaboratorList?.map((item) => {
return (
<Tr
key={item.tmbId}
_hover={{
bg: 'myGray.50'
}}
>
<Td border="none">
<Flex alignItems="center">
<Avatar src={item.avatar} rounded={'50%'} w="24px" mr={2} />
{item.name === DefaultGroupName ? userInfo?.team.teamName : item.name}
</Flex>
</Td>
<Td border="none">
<RoleTags permission={item.permission.role} />
</Td>
<Td border="none">
{/* Not self; Not owner and other manager */}
{item.tmbId !== userInfo?.team?.tmbId &&
(permission.isOwner || !item.permission.hasManagePer) && (
<RoleSelect
Button={
<MyIcon name={'edit'} w={'16px'} _hover={{ color: 'primary.600' }} />
}
value={item.permission.role}
onChange={(permission) => {
onUpdate({
members: item.tmbId ? [item.tmbId] : undefined,
groups: item.groupId ? [item.groupId] : undefined,
orgs: item.orgId ? [item.orgId] : undefined,
permission
});
}}
onDelete={() => {
onDelete({
tmbId: item.tmbId,
groupId: item.groupId,
orgId: item.orgId
} as RequireOnlyOne<{
tmbId: string;
groupId: string;
orgId: string;
}>);
}}
/>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
{collaboratorList?.length === 0 && <EmptyTip text={t('user:team.no_collaborators')} />}
</TableContainer>
{loading && <Loading fixed={false} />}
</ModalBody>
</MyModal>
);
}
export default ManageModal;
import React from 'react'; import React from 'react';
import { useTranslation } from 'next-i18next'; import { Box, Checkbox, Flex } from '@chakra-ui/react';
import { Box, Checkbox, HStack, VStack } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import RoleTags from './RoleTags'; import RoleTags from './RoleTags';
import type { RoleValueType } from '@fastgpt/global/support/permission/type'; import type { RoleValueType } from '@fastgpt/global/support/permission/type';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import OrgTags from '../../user/team/OrgTags'; import OrgTags from '../../user/team/OrgTags';
import Tag from '@fastgpt/web/components/common/Tag'; import RoleSelect from './RoleSelect';
import { ChevronDownIcon } from '@chakra-ui/icons';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { useUserStore } from '@/web/support/user/useUserStore';
function MemberItemCard({ function MemberItemCard({
avatar, avatar,
key, key,
onChange: _onChange, onChange,
isChecked, isChecked,
onDelete, onDelete,
name, name,
role, role,
orgs, orgs,
addOnly, rightSlot,
rightSlot onRoleChange,
disabled = false
}: { }: {
avatar: string; avatar: string;
key: string; key: string;
onChange: () => void; onChange?: () => void;
onRoleChange?: (role: RoleValueType) => void;
isChecked?: boolean; isChecked?: boolean;
onDelete?: () => void; onDelete?: () => void;
name: string; name: string;
role?: RoleValueType; role?: RoleValueType;
addOnly?: boolean;
orgs?: string[]; orgs?: string[];
rightSlot?: React.ReactNode; rightSlot?: React.ReactNode;
disabled?: boolean;
}) { }) {
const isAdded = addOnly && !!role; const showRoleSelect = onRoleChange !== undefined;
const onChange = () => { const { userInfo } = useUserStore();
if (!isAdded) _onChange();
};
const { t } = useTranslation();
return ( return (
<HStack <Flex
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"
key={key} key={key}
px="3" px="1"
py="2" py="1"
gap="2"
borderRadius="sm" borderRadius="sm"
_hover={{ {...(!showRoleSelect
bgColor: 'myGray.50', ? {
cursor: 'pointer' _hover: { bgColor: 'myGray.50' },
cursor: 'pointer'
}
: {})}
onClick={() => {
if (disabled) return;
onChange?.();
}} }}
onClick={onChange}
> >
{isChecked !== undefined && ( <Flex
<Checkbox isChecked={isChecked} pointerEvents="none" disabled={isAdded} /> flexDirection={'row'}
)} h={showRoleSelect ? '36px' : 'unset'}
<Avatar src={avatar} w="1.5rem" borderRadius={'50%'} /> p="1"
alignItems={'center'}
<Box w="full"> gap="2"
<Box fontSize={'sm'} className="textEllipsis" maxW="300px"> w="full"
{name} >
{isChecked !== undefined && (
<Checkbox isDisabled={disabled} isChecked={isChecked} pointerEvents="none" />
)}
<Avatar src={avatar} w="1.5rem" borderRadius={'50%'} />
<Box flex={'1 0 0'} w={0}>
<Box fontSize={'sm'} w={'100%'} noOfLines={1}>
{name === DefaultGroupName ? userInfo?.team.teamName : name}
</Box>
<Box lineHeight={1} w={'100%'}>
{orgs && orgs.length > 0 && <OrgTags orgs={orgs} />}
</Box>
</Box> </Box>
<Box lineHeight={1}>{orgs && orgs.length > 0 && <OrgTags orgs={orgs} />}</Box> </Flex>
</Box> {showRoleSelect && (
{!isAdded && role && <RoleTags permission={role} />} <RoleSelect
{isAdded && ( disabled={disabled}
<Tag value={role}
mixBlendMode={'multiply'} Button={
colorSchema="blue" <Flex
border="none" bg={'myGray.50'}
py={2} border="base"
px={3} fontSize={'sm'}
fontSize={'xs'} borderRadius={'md'}
> minH={'18px'}
{t('user:team.collaborator.added')} w="300px"
</Tag> p="1"
)} alignItems={'end'}
{onDelete !== undefined && ( justifyContent={'space-between'}
<MyIcon >
name="common/closeLight" <RoleTags permission={role} />
w="1rem" <Flex h="18px" alignItems={'center'} justifyContent={'center'}>
cursor={'pointer'} <ChevronDownIcon fontSize="md" />
_hover={{ </Flex>
color: 'red.600' </Flex>
}} }
onClick={onDelete} onChange={onRoleChange}
/> />
)} )}
{rightSlot} <Flex flexDirection={'row'} h={showRoleSelect ? '36px' : 'unset'} alignItems={'center'}>
</HStack> {onDelete !== undefined && !disabled ? (
<MyIcon
name="common/closeLight"
w="1rem"
cursor={disabled ? 'not-allowed' : 'pointer'}
_hover={{
color: 'red.600'
}}
onClick={() => {
if (disabled) return;
onDelete?.();
}}
/>
) : (
<Box minW="16px"></Box>
)}
</Flex>
{!!rightSlot && rightSlot}
</Flex>
); );
} }
......
...@@ -18,6 +18,7 @@ import { Permission } from '@fastgpt/global/support/permission/controller'; ...@@ -18,6 +18,7 @@ import { Permission } from '@fastgpt/global/support/permission/controller';
import { CollaboratorContext } from './context'; import { CollaboratorContext } from './context';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import MyDivider from '@fastgpt/web/components/common/MyDivider'; import MyDivider from '@fastgpt/web/components/common/MyDivider';
import { ManageRoleVal } from '@fastgpt/global/support/permission/constant';
export type PermissionSelectProps = { export type PermissionSelectProps = {
value?: RoleValueType; value?: RoleValueType;
...@@ -47,16 +48,15 @@ function RoleSelect({ ...@@ -47,16 +48,15 @@ function RoleSelect({
offset = [0, 5], offset = [0, 5],
Button, Button,
width = 'auto', width = 'auto',
onDelete onDelete,
disabled
}: PermissionSelectProps) { }: PermissionSelectProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const closeTimer = useRef<NodeJS.Timeout>(); const closeTimer = useRef<NodeJS.Timeout>();
const { permission, roleList: permissionList } = useContextSelector( const { roleList: permissionList } = useContextSelector(CollaboratorContext, (v) => v);
CollaboratorContext, const myRole = useContextSelector(CollaboratorContext, (v) => v.myRole);
(v) => v
);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
...@@ -72,15 +72,18 @@ function RoleSelect({ ...@@ -72,15 +72,18 @@ function RoleSelect({
}; };
}); });
const singleOptions = list.filter((item) => item.checkBoxType === 'single');
const per = new Permission({ role });
return { return {
singleOptions: list.filter( singleOptions: myRole.isOwner
(item) => ? singleOptions
item.checkBoxType === 'single' && : myRole.hasManagePer && !per.hasManagePer
(permission.isOwner || item.value !== permissionList['manage'].value) ? singleOptions.filter((item) => item.value !== ManageRoleVal)
), : [],
checkboxList: list.filter((item) => item.checkBoxType === 'multiple') checkboxList: list.filter((item) => item.checkBoxType === 'multiple')
}; };
}, [permission.isOwner, permissionList]); }, [myRole.hasManagePer, myRole.isOwner, permissionList, role]);
const selectedSingleValue = useMemo(() => { const selectedSingleValue = useMemo(() => {
if (!permissionList) return undefined; if (!permissionList) return undefined;
...@@ -120,6 +123,7 @@ function RoleSelect({ ...@@ -120,6 +123,7 @@ function RoleSelect({
ref={ref} ref={ref}
w="fit-content" w="fit-content"
onMouseEnter={() => { onMouseEnter={() => {
if (disabled) return;
if (trigger === 'hover') { if (trigger === 'hover') {
setIsOpen(true); setIsOpen(true);
} }
...@@ -135,8 +139,10 @@ function RoleSelect({ ...@@ -135,8 +139,10 @@ function RoleSelect({
> >
<MenuButton <MenuButton
position={'relative'} position={'relative'}
cursor={disabled ? 'not-allowed' : 'pointer'}
onClickCapture={() => { onClickCapture={() => {
if (trigger === 'click') { if (trigger === 'click') {
if (disabled) return;
setIsOpen(!isOpen); setIsOpen(!isOpen);
} }
}} }}
...@@ -158,10 +164,10 @@ function RoleSelect({ ...@@ -158,10 +164,10 @@ function RoleSelect({
{/* The list of single select permissions */} {/* The list of single select permissions */}
{roleOptions.singleOptions.map((item) => { {roleOptions.singleOptions.map((item) => {
const change = () => { const change = () => {
const per = new Permission({ role }); if (disabled) {
per.removeRole(selectedSingleValue); return;
per.addRole(item.value); }
onSelectRole(per.role); onSelectRole(item.value);
}; };
return ( return (
...@@ -187,7 +193,7 @@ function RoleSelect({ ...@@ -187,7 +193,7 @@ function RoleSelect({
); );
})} })}
{roleOptions.checkboxList.length > 0 && ( {roleOptions.checkboxList.length > 0 && roleOptions.singleOptions.length > 0 && (
<> <>
<MyDivider /> <MyDivider />
<Box pb="2" px="3" fontSize={'sm'} color={'myGray.900'}> <Box pb="2" px="3" fontSize={'sm'} color={'myGray.900'}>
...@@ -197,7 +203,8 @@ function RoleSelect({ ...@@ -197,7 +203,8 @@ function RoleSelect({
)} )}
{roleOptions.checkboxList.map((item) => { {roleOptions.checkboxList.map((item) => {
const change = () => { const change = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if ((e.target as HTMLElement).tagName === 'INPUT') return;
const per = new Permission({ role }); const per = new Permission({ role });
if (per.checkRole(item.value)) { if (per.checkRole(item.value)) {
per.removeRole(item.value); per.removeRole(item.value);
...@@ -216,9 +223,13 @@ function RoleSelect({ ...@@ -216,9 +223,13 @@ function RoleSelect({
} }
: {})} : {})}
{...MenuStyle} {...MenuStyle}
onClick={(e) => {
if (disabled) return;
change(e);
}}
> >
<Checkbox size="sm" isChecked={isChecked} onChange={change} /> <Checkbox size="sm" isChecked={isChecked} />
<Flex ml={4} flexDirection="column" flex={'1 0 0'} onClick={change}> <Flex ml={4} flexDirection="column" flex={'1 0 0'}>
<Box>{t(item.name as any)}</Box> <Box>{t(item.name as any)}</Box>
<Box color={'myGray.500'} fontSize={'mini'}> <Box color={'myGray.500'} fontSize={'mini'}>
{t(item.description as any)} {t(item.description as any)}
......
...@@ -19,16 +19,16 @@ function RoleTags({ permission }: PermissionTagsProp) { ...@@ -19,16 +19,16 @@ function RoleTags({ permission }: PermissionTagsProp) {
const roleTagList = getRoleLabelList(permission); const roleTagList = getRoleLabelList(permission);
return ( return (
<Flex gap="2" alignItems="center"> <Flex gap="2" alignItems="center" flexWrap="wrap" minH="15px">
{roleTagList.map((item) => ( {roleTagList.map((item) => (
<Tag <Tag
mixBlendMode={'multiply'} mixBlendMode={'multiply'}
key={item} key={item}
colorSchema="blue" colorSchema="blue"
border="none" border="none"
py={2} py={1}
px={3} px={2}
fontSize={'xs'} fontSize={'2xs'}
> >
{t(item as any)} {t(item as any)}
</Tag> </Tag>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment