Merge branch 'main' of github.com:nocobase/nocobase into huoshijie/fix-import-export-permission-bug

This commit is contained in:
aaaaaajie 2025-04-15 21:28:35 +08:00
commit 06d4b211a1
563 changed files with 14529 additions and 2713 deletions

View File

@ -5,6 +5,297 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.6.20](https://github.com/nocobase/nocobase/compare/v1.6.19...v1.6.20) - 2025-04-14
### 🎉 New Features
- **[Departments]** Make Department, Attachment URL, and Workflow response message plugins free ([#6663](https://github.com/nocobase/nocobase/pull/6663)) by @chenos
### 🐛 Bug Fixes
- **[client]**
- The filter form should not display the "Unsaved changes" prompt ([#6657](https://github.com/nocobase/nocobase/pull/6657)) by @zhangzhonghe
- "allow multiple" option not working for relation field ([#6661](https://github.com/nocobase/nocobase/pull/6661)) by @katherinehhh
- In the filter form, when the filter button is clicked, if there are fields that have not passed validation, the filtering is still triggered ([#6659](https://github.com/nocobase/nocobase/pull/6659)) by @zhangzhonghe
- Switching to the group menu should not jump to a page that has already been hidden in menu ([#6654](https://github.com/nocobase/nocobase/pull/6654)) by @zhangzhonghe
- **[File storage: S3(Pro)]**
- Organize language by @jiannx
- Individual baseurl and public settings, improve S3 pro storage config UX by @jiannx
- **[Migration manager]** the skip auto backup option becomes invalid if environment variable popup appears during migration by @gchust
## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14
### 🐛 Bug Fixes
- **[client]**
- Fix the issue of preview images being obscured ([#6651](https://github.com/nocobase/nocobase/pull/6651)) by @zhangzhonghe
- In the form block, the default value of the field configuration will first be displayed as the original variable string and then disappear ([#6649](https://github.com/nocobase/nocobase/pull/6649)) by @zhangzhonghe
## [v1.6.18](https://github.com/nocobase/nocobase/compare/v1.6.17...v1.6.18) - 2025-04-11
### 🚀 Improvements
- **[client]**
- Add default type fallback API for `Variable.Input` ([#6644](https://github.com/nocobase/nocobase/pull/6644)) by @mytharcher
- Optimize prompts for unconfigured pages ([#6641](https://github.com/nocobase/nocobase/pull/6641)) by @zhangzhonghe
- **[Workflow: Delay node]** Support to use variable for duration ([#6621](https://github.com/nocobase/nocobase/pull/6621)) by @mytharcher
- **[Workflow: Custom action event]** Add refresh settings for trigger workflow button by @mytharcher
### 🐛 Bug Fixes
- **[client]**
- subtable description overlapping with add new button ([#6646](https://github.com/nocobase/nocobase/pull/6646)) by @katherinehhh
- dashed underline caused by horizontal form layout in modal ([#6639](https://github.com/nocobase/nocobase/pull/6639)) by @katherinehhh
- **[File storage: S3(Pro)]** Fix missing await for next call. by @jiannx
- **[Email manager]** Fix missing await for next call. by @jiannx
## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09
### 🚀 Improvements
- **[utils]** Add duration extension for dayjs ([#6630](https://github.com/nocobase/nocobase/pull/6630)) by @mytharcher
- **[client]**
- Support to search field in Filter component ([#6627](https://github.com/nocobase/nocobase/pull/6627)) by @mytharcher
- Add `trim` API for `Input` and `Variable.TextArea` ([#6624](https://github.com/nocobase/nocobase/pull/6624)) by @mytharcher
- **[Error handler]** Support custom title in AppError component. ([#6409](https://github.com/nocobase/nocobase/pull/6409)) by @sheldon66
- **[IP restriction]** Update IP restriction message content. by @sheldon66
- **[File storage: S3(Pro)]** Support global variables in storage configuration by @mytharcher
### 🐛 Bug Fixes
- **[client]**
- rule with 'any' condition does not take effect when condition list is empty ([#6628](https://github.com/nocobase/nocobase/pull/6628)) by @katherinehhh
- data issue with Gantt block in tree collection ([#6617](https://github.com/nocobase/nocobase/pull/6617)) by @katherinehhh
- The relationship fields in the filter form report an error after the page is refreshed because x-data-source is not carried ([#6619](https://github.com/nocobase/nocobase/pull/6619)) by @zhangzhonghe
- variable parse failure when URL parameters contain Chinese characters ([#6618](https://github.com/nocobase/nocobase/pull/6618)) by @katherinehhh
- **[Users]** Issue with parsing the user profile form schema ([#6635](https://github.com/nocobase/nocobase/pull/6635)) by @2013xile
- **[Mobile]** single-select field with 'contains' filter on mobile does not support multiple selection ([#6629](https://github.com/nocobase/nocobase/pull/6629)) by @katherinehhh
- **[Action: Export records]** missing filter params when exporting data after changing pagination ([#6633](https://github.com/nocobase/nocobase/pull/6633)) by @katherinehhh
- **[Email manager]** fix email management permission cannot view email list by @jiannx
- **[File storage: S3(Pro)]** Throw error to user when upload logo to S3 Pro storage (set to default) by @mytharcher
- **[Workflow: Approval]** Fix `updatedAt` changed after migration by @mytharcher
- **[Migration manager]** migration log creation time is displayed incorrectly in some environments by @gchust
## [v1.6.16](https://github.com/nocobase/nocobase/compare/v1.6.15...v1.6.16) - 2025-04-03
### 🐛 Bug Fixes
- **[client]**
- x-disabled property not taking effect on form fields ([#6610](https://github.com/nocobase/nocobase/pull/6610)) by @katherinehhh
- field label display issue to prevent truncation by colon ([#6599](https://github.com/nocobase/nocobase/pull/6599)) by @katherinehhh
- **[database]** When deleting one-to-many records, both `filter` and `filterByTk` are passed and `filter` includes an association field, the `filterByTk` is ignored ([#6606](https://github.com/nocobase/nocobase/pull/6606)) by @2013xile
## [v1.6.15](https://github.com/nocobase/nocobase/compare/v1.6.14...v1.6.15) - 2025-04-01
### 🚀 Improvements
- **[database]**
- Add trim option for text field ([#6603](https://github.com/nocobase/nocobase/pull/6603)) by @mytharcher
- Add trim option for string field ([#6565](https://github.com/nocobase/nocobase/pull/6565)) by @mytharcher
- **[File manager]** Add trim option for text fields of storages collection ([#6604](https://github.com/nocobase/nocobase/pull/6604)) by @mytharcher
- **[Workflow]** Improve code ([#6589](https://github.com/nocobase/nocobase/pull/6589)) by @mytharcher
- **[Workflow: Approval]** Support to use block template for approval process form by @mytharcher
### 🐛 Bug Fixes
- **[database]** Avoid "datetimeNoTz" field changes when value not changed in updating record ([#6588](https://github.com/nocobase/nocobase/pull/6588)) by @mytharcher
- **[client]**
- association field (select) displaying N/A when exposing related collection fields ([#6582](https://github.com/nocobase/nocobase/pull/6582)) by @katherinehhh
- Fix `disabled` property not works when `SchemaInitializerItem` has `items` ([#6597](https://github.com/nocobase/nocobase/pull/6597)) by @mytharcher
- cascade issue: 'The value of xxx cannot be in array format' when deleting and re-selecting ([#6585](https://github.com/nocobase/nocobase/pull/6585)) by @katherinehhh
- **[Collection field: Many to many (array)]** Issue of filtering by fields in an association collection with a many to many (array) field ([#6596](https://github.com/nocobase/nocobase/pull/6596)) by @2013xile
- **[Public forms]** View permissions include list and get ([#6607](https://github.com/nocobase/nocobase/pull/6607)) by @chenos
- **[Authentication]** token assignment in `AuthProvider` ([#6593](https://github.com/nocobase/nocobase/pull/6593)) by @2013xile
- **[Workflow]** Fix sync option display incorrectly ([#6595](https://github.com/nocobase/nocobase/pull/6595)) by @mytharcher
- **[Block: Map]** map management validation should not pass with space input ([#6575](https://github.com/nocobase/nocobase/pull/6575)) by @katherinehhh
- **[Workflow: Approval]**
- Fix client variables to use in approval form by @mytharcher
- Fix branch mode when `endOnReject` configured as `true` by @mytharcher
## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29
### 🐛 Bug Fixes
- **[Calendar]** missing data on boundary dates in weekly calendar view ([#6587](https://github.com/nocobase/nocobase/pull/6587)) by @katherinehhh
- **[Auth: OIDC]** Incorrect redirection occurs when the callback path is the string 'null' by @2013xile
- **[Workflow: Approval]** Fix approval node configuration is incorrect after schema changed by @mytharcher
## [v1.6.13](https://github.com/nocobase/nocobase/compare/v1.6.12...v1.6.13) - 2025-03-28
### 🚀 Improvements
- **[Async task manager]** optimize import/export buttons in Pro ([#6531](https://github.com/nocobase/nocobase/pull/6531)) by @chenos
- **[Action: Export records Pro]** optimize import/export buttons in Pro by @katherinehhh
- **[Migration manager]** allow skip automatic backup and restore for migration by @gchust
### 🐛 Bug Fixes
- **[client]** linkage conflict between same-named association fields in different sub-tables within the same form ([#6577](https://github.com/nocobase/nocobase/pull/6577)) by @katherinehhh
- **[Action: Batch edit]** Click the batch edit button, configure the pop-up window, and then open it again, the pop-up window is blank ([#6578](https://github.com/nocobase/nocobase/pull/6578)) by @zhangzhonghe
## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27
### 🐛 Bug Fixes
- **[Block: Multi-step form]**
- the submit button has the same color in its default and highlighted by @jiannx
- fixed the bug that form reset is invalid when the field is associated with other field by @jiannx
- **[Workflow: Approval]** Fix approval form values to submit by @mytharcher
## [v1.6.11](https://github.com/nocobase/nocobase/compare/v1.6.10...v1.6.11) - 2025-03-27
### 🚀 Improvements
- **[client]**
- Optimize 502 error message ([#6547](https://github.com/nocobase/nocobase/pull/6547)) by @chenos
- Only support plain text file to preview ([#6563](https://github.com/nocobase/nocobase/pull/6563)) by @mytharcher
- **[Collection field: Sequence]** support setting sequence as the title field for calendar block ([#6562](https://github.com/nocobase/nocobase/pull/6562)) by @katherinehhh
- **[Workflow: Approval]** Support to skip validator in settings by @mytharcher
### 🐛 Bug Fixes
- **[client]**
- issue with date field display in data scope filtering ([#6564](https://github.com/nocobase/nocobase/pull/6564)) by @katherinehhh
- The 'Ellipsis overflow content' option requires a page refresh for the toggle state to take effect ([#6520](https://github.com/nocobase/nocobase/pull/6520)) by @zhangzhonghe
- Unable to open another modal within a modal ([#6535](https://github.com/nocobase/nocobase/pull/6535)) by @zhangzhonghe
- **[API documentation]** API document page cannot scroll ([#6566](https://github.com/nocobase/nocobase/pull/6566)) by @zhangzhonghe
- **[Workflow]** Make sure workflow key is generated before save ([#6567](https://github.com/nocobase/nocobase/pull/6567)) by @mytharcher
- **[Workflow: Post-action event]** Multiple records in bulk action should trigger multiple times ([#6559](https://github.com/nocobase/nocobase/pull/6559)) by @mytharcher
- **[Authentication]** Localization issue for fields of sign up page ([#6556](https://github.com/nocobase/nocobase/pull/6556)) by @2013xile
- **[Public forms]** issue with public form page title displaying 'Loading...' ([#6569](https://github.com/nocobase/nocobase/pull/6569)) by @katherinehhh
## [v1.6.10](https://github.com/nocobase/nocobase/compare/v1.6.9...v1.6.10) - 2025-03-25
### 🐛 Bug Fixes
- **[client]**
- Unable to use 'Current User' variable when adding a link page ([#6536](https://github.com/nocobase/nocobase/pull/6536)) by @zhangzhonghe
- field assignment with null value is ineffective ([#6549](https://github.com/nocobase/nocobase/pull/6549)) by @katherinehhh
- `yarn doc` command error ([#6540](https://github.com/nocobase/nocobase/pull/6540)) by @gchust
- Remove the 'Allow multiple selection' option from dropdown single-select fields in filter forms ([#6515](https://github.com/nocobase/nocobase/pull/6515)) by @zhangzhonghe
- Relational field's data range linkage is not effective ([#6530](https://github.com/nocobase/nocobase/pull/6530)) by @zhangzhonghe
- **[Collection: Tree]** Migration issue for plugin-collection-tree ([#6537](https://github.com/nocobase/nocobase/pull/6537)) by @2013xile
- **[Action: Custom request]** Unable to download UTF-8 encoded files ([#6541](https://github.com/nocobase/nocobase/pull/6541)) by @2013xile
## [v1.6.9](https://github.com/nocobase/nocobase/compare/v1.6.8...v1.6.9) - 2025-03-23
### 🐛 Bug Fixes
- **[client]** action button transparency causing setting display issue on hover ([#6529](https://github.com/nocobase/nocobase/pull/6529)) by @katherinehhh
## [v1.6.8](https://github.com/nocobase/nocobase/compare/v1.6.7...v1.6.8) - 2025-03-22
### 🐛 Bug Fixes
- **[server]** The upgrade command may cause workflow errors ([#6524](https://github.com/nocobase/nocobase/pull/6524)) by @gchust
- **[client]** the height of the subtable in the form is set along with the form height ([#6518](https://github.com/nocobase/nocobase/pull/6518)) by @katherinehhh
- **[Authentication]**
- X-Authenticator missing ([#6526](https://github.com/nocobase/nocobase/pull/6526)) by @chenos
- Trim authenticator options ([#6527](https://github.com/nocobase/nocobase/pull/6527)) by @2013xile
- **[Block: Map]** map block key management issue causing request failures due to invisible characters ([#6521](https://github.com/nocobase/nocobase/pull/6521)) by @katherinehhh
- **[Backup manager]** Restoration may cause workflow execution errors by @gchust
- **[WeCom]** Resolve environment variables and secrets when retrieving notification configuration. by @2013xile
## [v1.6.7](https://github.com/nocobase/nocobase/compare/v1.6.6...v1.6.7) - 2025-03-20
### 🚀 Improvements
- **[Workflow: mailer node]** Add secure field config description. ([#6510](https://github.com/nocobase/nocobase/pull/6510)) by @sheldon66
- **[Notification: Email]** Add secure field config description. ([#6501](https://github.com/nocobase/nocobase/pull/6501)) by @sheldon66
- **[Calendar]** Calendar plugin with optional settings to enable or disable quick event creation ([#6391](https://github.com/nocobase/nocobase/pull/6391)) by @Cyx649312038
### 🐛 Bug Fixes
- **[client]** time field submission error in Chinese locale (invalid input syntax for type time) ([#6511](https://github.com/nocobase/nocobase/pull/6511)) by @katherinehhh
- **[File manager]** Unable to access files stored in COS ([#6512](https://github.com/nocobase/nocobase/pull/6512)) by @chenos
- **[Block: Map]** secret key fields not triggering validation in map management ([#6509](https://github.com/nocobase/nocobase/pull/6509)) by @katherinehhh
- **[WEB client]** The path in the route management table is different from the actual path ([#6483](https://github.com/nocobase/nocobase/pull/6483)) by @zhangzhonghe
- **[Action: Export records Pro]** Unable to export attachments by @chenos
- **[Workflow: Approval]**
- Fix null user caused crash by @mytharcher
- Fix error thrown when add query node result by @mytharcher
## [v1.6.6](https://github.com/nocobase/nocobase/compare/v1.6.5...v1.6.6) - 2025-03-18 ## [v1.6.6](https://github.com/nocobase/nocobase/compare/v1.6.5...v1.6.6) - 2025-03-18
### 🎉 New Features ### 🎉 New Features

View File

@ -5,6 +5,297 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
## [v1.6.20](https://github.com/nocobase/nocobase/compare/v1.6.19...v1.6.20) - 2025-04-14
### 🎉 新特性
- **[部门]** 商业插件部门、附件 URL、工作流响应消息改为免费提供 ([#6663](https://github.com/nocobase/nocobase/pull/6663)) by @chenos
### 🐛 修复
- **[client]**
- 筛选表单不应该显示“未保存修改”提示 ([#6657](https://github.com/nocobase/nocobase/pull/6657)) by @zhangzhonghe
- 筛选表单中关系字段的“允许多选”设置项不生效 ([#6661](https://github.com/nocobase/nocobase/pull/6661)) by @katherinehhh
- 筛选表单中,当点击筛选按钮时,如果有字段未校验通过,依然会触发筛选的问题 ([#6659](https://github.com/nocobase/nocobase/pull/6659)) by @zhangzhonghe
- 切换到分组菜单时,不应该跳转到已经在菜单中被隐藏的页面 ([#6654](https://github.com/nocobase/nocobase/pull/6654)) by @zhangzhonghe
- **[文件存储S3 (Pro)]**
- 整理语言文案 by @jiannx
- baseurl 和 public 设置不再互相关联,改进 S3 pro 存储的配置交互体验 by @jiannx
- **[迁移管理]** 迁移时若弹出环境变量弹窗,跳过自动备份选项会失效 by @gchust
## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14
### 🐛 修复
- **[client]**
- 修复预览图片被遮挡的问题 ([#6651](https://github.com/nocobase/nocobase/pull/6651)) by @zhangzhonghe
- 表单区块中,字段配置的默认值会先显示为原始变量字符串然后再消失 ([#6649](https://github.com/nocobase/nocobase/pull/6649)) by @zhangzhonghe
## [v1.6.18](https://github.com/nocobase/nocobase/compare/v1.6.17...v1.6.18) - 2025-04-11
### 🚀 优化
- **[client]**
- 为 `Variable.Input` 组件增加默认退避类型的 API ([#6644](https://github.com/nocobase/nocobase/pull/6644)) by @mytharcher
- 优化未配置页面时的提示 ([#6641](https://github.com/nocobase/nocobase/pull/6641)) by @zhangzhonghe
- **[工作流:延时节点]** 支持延迟时间使用变量 ([#6621](https://github.com/nocobase/nocobase/pull/6621)) by @mytharcher
- **[工作流:自定义操作事件]** 为触发工作流按钮增加刷新配置项 by @mytharcher
### 🐛 修复
- **[client]**
- 子表格中描述信息与操作按钮遮挡 ([#6646](https://github.com/nocobase/nocobase/pull/6646)) by @katherinehhh
- 弹窗表单在 horizontal 布局下初始宽度计算错误,导致出现提示和 下划虚线 ([#6639](https://github.com/nocobase/nocobase/pull/6639)) by @katherinehhh
- **[文件存储S3 (Pro)]** 修复next调用缺少await by @jiannx
- **[邮件管理]** 修复next调用缺少await by @jiannx
## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09
### 🚀 优化
- **[utils]** 为 dayjs 包增加时长扩展 ([#6630](https://github.com/nocobase/nocobase/pull/6630)) by @mytharcher
- **[client]**
- 支持筛选组件中对字段进行搜索 ([#6627](https://github.com/nocobase/nocobase/pull/6627)) by @mytharcher
- 为 `Input``Variable.TextArea` 组件增加 `trim` API ([#6624](https://github.com/nocobase/nocobase/pull/6624)) by @mytharcher
- **[错误处理器]** 在 AppError 组件中支持自定义标题。 ([#6409](https://github.com/nocobase/nocobase/pull/6409)) by @sheldon66
- **[IP 限制]** 更新 IP 限制消息内容。 by @sheldon66
- **[文件存储S3 (Pro)]** 支持存储引擎的配置中使用全局变量 by @mytharcher
### 🐛 修复
- **[client]**
- 联动规则条件设置为任意且无条件内容时属性设置不生效 ([#6628](https://github.com/nocobase/nocobase/pull/6628)) by @katherinehhh
- 树表使用甘特图区块时数据显示异常 ([#6617](https://github.com/nocobase/nocobase/pull/6617)) by @katherinehhh
- 筛选表单中的关系字段在刷新页面后,由于没有携带 x-data-source 而报错 ([#6619](https://github.com/nocobase/nocobase/pull/6619)) by @zhangzhonghe
- 链接中中文参数变量值解析失败 ([#6618](https://github.com/nocobase/nocobase/pull/6618)) by @katherinehhh
- **[用户]** 用户个人资料表单 schema 的解析问题 ([#6635](https://github.com/nocobase/nocobase/pull/6635)) by @2013xile
- **[移动端]** 下拉单选字段在移动端设置筛选符为包含时组件未支持多选 ([#6629](https://github.com/nocobase/nocobase/pull/6629)) by @katherinehhh
- **[操作:导出记录]** 筛选数据后切换分页再导出时筛选参数丢失 ([#6633](https://github.com/nocobase/nocobase/pull/6633)) by @katherinehhh
- **[邮件管理]** 邮件管理权限无法查看邮件列表 by @jiannx
- **[文件存储S3 (Pro)]** 当用户上传 logo 失败时提示错误(设置为默认存储的 S3 Pro by @mytharcher
- **[工作流:审批]** 修复更新时间在迁移后变化 by @mytharcher
- **[迁移管理]** 部分服务器环境下迁移日志创建日期显示不正确 by @gchust
## [v1.6.16](https://github.com/nocobase/nocobase/compare/v1.6.15...v1.6.16) - 2025-04-03
### 🐛 修复
- **[client]**
- 表单字段设置不可编辑不起作用 ([#6610](https://github.com/nocobase/nocobase/pull/6610)) by @katherinehhh
- 表单字段标题因冒号导致的截断问题 ([#6599](https://github.com/nocobase/nocobase/pull/6599)) by @katherinehhh
- **[database]** 删除一对多记录时,同时传递 `filter``filterByTk` 参数,`filter` 包含关系字段时,`filterByTk` 参数失效 ([#6606](https://github.com/nocobase/nocobase/pull/6606)) by @2013xile
## [v1.6.15](https://github.com/nocobase/nocobase/compare/v1.6.14...v1.6.15) - 2025-04-01
### 🚀 优化
- **[database]**
- 为多行文本类型字段增加去除首尾空白字符的选项 ([#6603](https://github.com/nocobase/nocobase/pull/6603)) by @mytharcher
- 为单行文本增加自动去除首尾空白字符的选项 ([#6565](https://github.com/nocobase/nocobase/pull/6565)) by @mytharcher
- **[文件管理器]** 为存储引擎表的文本字段增加去除首尾空白字符的选项 ([#6604](https://github.com/nocobase/nocobase/pull/6604)) by @mytharcher
- **[工作流]** 优化代码 ([#6589](https://github.com/nocobase/nocobase/pull/6589)) by @mytharcher
- **[工作流:审批]** 支持审批表单使用区块模板 by @mytharcher
### 🐛 修复
- **[database]** 避免“日期时间(无时区)”字段在值未变动的更新时触发值改变 ([#6588](https://github.com/nocobase/nocobase/pull/6588)) by @mytharcher
- **[client]**
- 关系字段select放出关系表字段时默认显示 N/A ([#6582](https://github.com/nocobase/nocobase/pull/6582)) by @katherinehhh
- 修复 `SchemaInitializerItem` 配置了 `items``disabled` 属性无效的问题 ([#6597](https://github.com/nocobase/nocobase/pull/6597)) by @mytharcher
- 级联组件删除后重新选择时出现 'The value of xxx cannot be in array format' ([#6585](https://github.com/nocobase/nocobase/pull/6585)) by @katherinehhh
- **[数据表字段:多对多 (数组)]** 主表筛选带有多对多(数组)字段的关联表中的字段报错的问题 ([#6596](https://github.com/nocobase/nocobase/pull/6596)) by @2013xile
- **[公开表单]** 查看权限包括 list 和 get ([#6607](https://github.com/nocobase/nocobase/pull/6607)) by @chenos
- **[用户认证]** `AuthProvider` 中的 token 赋值 ([#6593](https://github.com/nocobase/nocobase/pull/6593)) by @2013xile
- **[工作流]** 修复同步选项展示问题 ([#6595](https://github.com/nocobase/nocobase/pull/6595)) by @mytharcher
- **[区块:地图]** 地图管理必填校验不应通过空格输入 ([#6575](https://github.com/nocobase/nocobase/pull/6575)) by @katherinehhh
- **[工作流:审批]**
- 修复审批表单中的前端变量 by @mytharcher
- 修复分支模式下配置拒绝则结束时的流程问题 by @mytharcher
## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29
### 🐛 修复
- **[日历]** 日历区块以周为视图时,边界日期不显示数据 ([#6587](https://github.com/nocobase/nocobase/pull/6587)) by @katherinehhh
- **[认证OIDC]** 回调路径是字符串'null'时导致跳转不正确 by @2013xile
- **[工作流:审批]** 修复审批节点界面配置变更后数据未同步的问题 by @mytharcher
## [v1.6.13](https://github.com/nocobase/nocobase/compare/v1.6.12...v1.6.13) - 2025-03-28
### 🚀 优化
- **[异步任务管理器]** 优化 Pro 导入导出按钮异步逻辑 ([#6531](https://github.com/nocobase/nocobase/pull/6531)) by @chenos
- **[操作:导出记录 Pro]** 优化 Pro 导入导出按钮 by @katherinehhh
- **[迁移管理]** 允许执行迁移时跳过自动备份还原 by @gchust
### 🐛 修复
- **[client]** 同一表单中不同关系字段的同名关系字段的联动互相影响 ([#6577](https://github.com/nocobase/nocobase/pull/6577)) by @katherinehhh
- **[操作:批量编辑]** 点击批量编辑按钮,配置完弹窗再打开,弹窗是空白的 ([#6578](https://github.com/nocobase/nocobase/pull/6578)) by @zhangzhonghe
## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27
### 🐛 修复
- **[区块:分步表单]**
- 提交按钮默认和高亮情况下颜色一样 by @jiannx
- 修复当字段与其他表单字段存在关联时,表单重置无效 by @jiannx
- **[工作流:审批]** 修复审批表单提交值的问题 by @mytharcher
## [v1.6.11](https://github.com/nocobase/nocobase/compare/v1.6.10...v1.6.11) - 2025-03-27
### 🚀 优化
- **[client]**
- 优化 502 错误提示 ([#6547](https://github.com/nocobase/nocobase/pull/6547)) by @chenos
- 仅支持纯文本文件预览 ([#6563](https://github.com/nocobase/nocobase/pull/6563)) by @mytharcher
- **[数据表字段:自动编码]** 支持使用 sequence 作为日历区块的标题字段 ([#6562](https://github.com/nocobase/nocobase/pull/6562)) by @katherinehhh
- **[工作流:审批]** 支持审批处理按钮跳过表单验证的设置 by @mytharcher
### 🐛 修复
- **[client]**
- 数据范围中筛选日期字段显示异常 ([#6564](https://github.com/nocobase/nocobase/pull/6564)) by @katherinehhh
- 选项“省略超出长度的内容”需要刷新页面,开关的状态才生效 ([#6520](https://github.com/nocobase/nocobase/pull/6520)) by @zhangzhonghe
- 在弹窗中无法再次打开弹窗 ([#6535](https://github.com/nocobase/nocobase/pull/6535)) by @zhangzhonghe
- **[API 文档]** API 文档页面不能滚动 ([#6566](https://github.com/nocobase/nocobase/pull/6566)) by @zhangzhonghe
- **[工作流]** 确保创建工作流之前 key 已生成 ([#6567](https://github.com/nocobase/nocobase/pull/6567)) by @mytharcher
- **[工作流:操作后事件]** 多行记录的批量操作需要触发多次 ([#6559](https://github.com/nocobase/nocobase/pull/6559)) by @mytharcher
- **[用户认证]** 注册页面字段的本地化问题 ([#6556](https://github.com/nocobase/nocobase/pull/6556)) by @2013xile
- **[公开表单]** 公开表单页面标题不应该显示 Loading... ([#6569](https://github.com/nocobase/nocobase/pull/6569)) by @katherinehhh
## [v1.6.10](https://github.com/nocobase/nocobase/compare/v1.6.9...v1.6.10) - 2025-03-25
### 🐛 修复
- **[client]**
- 添加链接页面时,无法使用“当前用户”变量 ([#6536](https://github.com/nocobase/nocobase/pull/6536)) by @zhangzhonghe
- 字段赋值对字段进行“空值”赋值无效 ([#6549](https://github.com/nocobase/nocobase/pull/6549)) by @katherinehhh
- `yarn doc` 命令报错 ([#6540](https://github.com/nocobase/nocobase/pull/6540)) by @gchust
- 筛选表单中,移除下拉单选字段的“允许多选”选项 ([#6515](https://github.com/nocobase/nocobase/pull/6515)) by @zhangzhonghe
- 关系字段的数据范围联动不生效 ([#6530](https://github.com/nocobase/nocobase/pull/6530)) by @zhangzhonghe
- **[数据表:树]** 树表插件的迁移脚本问题 ([#6537](https://github.com/nocobase/nocobase/pull/6537)) by @2013xile
- **[操作:自定义请求]** 无法下载utf8编码的文件 ([#6541](https://github.com/nocobase/nocobase/pull/6541)) by @2013xile
## [v1.6.9](https://github.com/nocobase/nocobase/compare/v1.6.8...v1.6.9) - 2025-03-23
### 🐛 修复
- **[client]** 操作按钮透明状态导致 hover 时按钮 setting 显示异常 ([#6529](https://github.com/nocobase/nocobase/pull/6529)) by @katherinehhh
## [v1.6.8](https://github.com/nocobase/nocobase/compare/v1.6.7...v1.6.8) - 2025-03-22
### 🐛 修复
- **[server]** Upgrade 命令可能造成工作流报错 ([#6524](https://github.com/nocobase/nocobase/pull/6524)) by @gchust
- **[client]** 表单中的子表格高度会随主表单高度一同设置 ([#6518](https://github.com/nocobase/nocobase/pull/6518)) by @katherinehhh
- **[用户认证]**
- X-Authenticator 缺失 ([#6526](https://github.com/nocobase/nocobase/pull/6526)) by @chenos
- 移除认证器配置项前后的空格、换行符 ([#6527](https://github.com/nocobase/nocobase/pull/6527)) by @2013xile
- **[区块:地图]** 地图区块 密钥管理中不可见字符导致的密钥请求失败的问题 ([#6521](https://github.com/nocobase/nocobase/pull/6521)) by @katherinehhh
- **[备份管理器]** 还原过程中可能引起工作流执行报错 by @gchust
- **[企业微信]** 获取通知配置时需要解析环境变量和密钥 by @2013xile
## [v1.6.7](https://github.com/nocobase/nocobase/compare/v1.6.6...v1.6.7) - 2025-03-20
### 🚀 优化
- **[工作流:邮件发送节点]** 增加安全字段配置描述。 ([#6510](https://github.com/nocobase/nocobase/pull/6510)) by @sheldon66
- **[通知:电子邮件]** 增加安全字段配置描述。 ([#6501](https://github.com/nocobase/nocobase/pull/6501)) by @sheldon66
- **[日历]** 日历插件添加开启或关闭快速创建事件可选设置 ([#6391](https://github.com/nocobase/nocobase/pull/6391)) by @Cyx649312038
### 🐛 修复
- **[client]** 时间字段在中文语言下提交时报错 invalid input syntax for type time ([#6511](https://github.com/nocobase/nocobase/pull/6511)) by @katherinehhh
- **[文件管理器]** COS 存储的文件无法访问 ([#6512](https://github.com/nocobase/nocobase/pull/6512)) by @chenos
- **[区块:地图]** 地图管理中密钥必填校验失败 ([#6509](https://github.com/nocobase/nocobase/pull/6509)) by @katherinehhh
- **[WEB 客户端]** 路由管理表格中的路径与实际路径不一样 ([#6483](https://github.com/nocobase/nocobase/pull/6483)) by @zhangzhonghe
- **[操作:导出记录 Pro]** 无法导出附件 by @chenos
- **[工作流:审批]**
- 修复空用户造成页面崩溃 by @mytharcher
- 修复审批人界面配置添加查询节点时的页面崩溃 by @mytharcher
## [v1.6.6](https://github.com/nocobase/nocobase/compare/v1.6.5...v1.6.6) - 2025-03-18 ## [v1.6.6](https://github.com/nocobase/nocobase/compare/v1.6.5...v1.6.6) - 2025-03-18
### 🎉 新特性 ### 🎉 新特性

View File

@ -1,4 +1,4 @@
Updated Date: February 20, 2025 Updated Date: April 1, 2025
NocoBase License Agreement NocoBase License Agreement
@ -88,7 +88,7 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
6.6 Can sell plugins developed for Software in the Marketplace. 6.6 Can sell plugins developed for Software in the Marketplace.
6.7 The User with an Enterprise Edition License can sell Upper Layer Application to their clients. 6.7 The User with a Professional or Enterprise Edition License can sell Upper Layer Application to their clients.
6.8 Not restricted by the AGPL-3.0 agreement. 6.8 Not restricted by the AGPL-3.0 agreement.
@ -106,9 +106,9 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
7.4 It is not allowed to provide any form of no-code, zero-code, low-code platform SaaS products to the public using the original or modified Software. 7.4 It is not allowed to provide any form of no-code, zero-code, low-code platform SaaS products to the public using the original or modified Software.
7.5 It is not allowed for the User withot an Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license. 7.5 It is not allowed for the User withot a Professional or Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license.
7.6 It is not allowed for the User with an Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license with access to further development and configuration. 7.6 It is not allowed for the User with a Professional or Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license with access to further development and configuration.
7.7 It is not allowed to publicly sell plugins developed for Software outside of the Marketplace. 7.7 It is not allowed to publicly sell plugins developed for Software outside of the Marketplace.

View File

@ -2,14 +2,10 @@
https://github.com/user-attachments/assets/cf08bfe5-e6e6-453c-8b96-350a6a8bed17 https://github.com/user-attachments/assets/cf08bfe5-e6e6-453c-8b96-350a6a8bed17
## ご協力ありがとうございます! <p align="center">
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability&#0045;first&#0044;&#0032;open&#0045;source&#0032;no&#0045;code&#0032;platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability&#0045;first&#0044;&#0032;open&#0045;source&#0032;no&#0045;code&#0032;platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p>
## リリースノート
リリースノートは[ブログ](https://www.nocobase.com/ja/blog/timeline)で随時更新され、週ごとにまとめて公開しています。
## NocoBaseはなに ## NocoBaseはなに
@ -28,6 +24,16 @@ https://docs-cn.nocobase.com/
コミュニティ: コミュニティ:
https://forum.nocobase.com/ https://forum.nocobase.com/
チュートリアル:
https://www.nocobase.com/ja/tutorials
顧客のストーリー:
https://www.nocobase.com/ja/blog/tags/customer-stories
## リリースノート
リリースノートは[ブログ](https://www.nocobase.com/ja/blog/timeline)で随時更新され、週ごとにまとめて公開しています。
## 他の製品との違い ## 他の製品との違い
### 1. データモデル駆動 ### 1. データモデル駆動

View File

@ -2,19 +2,14 @@ English | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
https://github.com/user-attachments/assets/a50c100a-4561-4e06-b2d2-d48098659ec0 https://github.com/user-attachments/assets/a50c100a-4561-4e06-b2d2-d48098659ec0
## We'd love your support! <p align="center">
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability&#0045;first&#0044;&#0032;open&#0045;source&#0032;no&#0045;code&#0032;platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability&#0045;first&#0044;&#0032;open&#0045;source&#0032;no&#0045;code&#0032;platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p>
## Release Notes
Our [blog](https://www.nocobase.com/en/blog/timeline) is regularly updated with release notes and provides a weekly summary.
## What is NocoBase ## What is NocoBase
NocoBase is a scalability-first, open-source no-code development platform. NocoBase is an extensibility-first, open-source no-code development platform.
Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform! Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform!
Homepage: Homepage:
@ -29,6 +24,17 @@ https://docs.nocobase.com/
Forum: Forum:
https://forum.nocobase.com/ https://forum.nocobase.com/
Tutorials:
https://www.nocobase.com/en/tutorials
Use Cases:
https://www.nocobase.com/en/blog/tags/customer-stories
## Release Notes
Our [blog](https://www.nocobase.com/en/blog/timeline) is regularly updated with release notes and provides a weekly summary.
## Distinctive features ## Distinctive features
### 1. Data model-driven ### 1. Data model-driven

View File

@ -2,13 +2,10 @@
https://github.com/nocobase/nocobase/assets/1267426/29623e45-9a48-4598-bb9e-9dd173ade553 https://github.com/nocobase/nocobase/assets/1267426/29623e45-9a48-4598-bb9e-9dd173ade553
## 感谢支持 <p align="center">
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability&#0045;first&#0044;&#0032;open&#0045;source&#0032;no&#0045;code&#0032;platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability&#0045;first&#0044;&#0032;open&#0045;source&#0032;no&#0045;code&#0032;platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p>
## 发布日志
我们的[博客](https://www.nocobase.com/cn/blog/timeline)会及时更新发布日志,并每周进行汇总。
## NocoBase 是什么 ## NocoBase 是什么
@ -27,6 +24,15 @@ https://docs-cn.nocobase.com/
社区: 社区:
https://forum.nocobase.com/ https://forum.nocobase.com/
教程:
https://www.nocobase.com/cn/tutorials
用户故事:
https://www.nocobase.com/cn/blog/tags/customer-stories
## 发布日志
我们的[博客](https://www.nocobase.com/cn/blog/timeline)会及时更新发布日志,并每周进行汇总。
## 与众不同之处 ## 与众不同之处
### 1. 数据模型驱动 ### 1. 数据模型驱动

View File

@ -1,5 +1,5 @@
{ {
"version": "1.6.6", "version": "1.6.20",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"npmClientArgs": ["--ignore-engines"], "npmClientArgs": ["--ignore-engines"],

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/acl", "name": "@nocobase/acl",
"version": "1.6.6", "version": "1.6.20",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/resourcer": "1.6.6", "@nocobase/resourcer": "1.6.20",
"@nocobase/utils": "1.6.6", "@nocobase/utils": "1.6.20",
"minimatch": "^5.1.1" "minimatch": "^5.1.1"
}, },
"repository": { "repository": {

View File

@ -1,14 +1,14 @@
{ {
"name": "@nocobase/actions", "name": "@nocobase/actions",
"version": "1.6.6", "version": "1.6.20",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/cache": "1.6.6", "@nocobase/cache": "1.6.20",
"@nocobase/database": "1.6.6", "@nocobase/database": "1.6.20",
"@nocobase/resourcer": "1.6.6" "@nocobase/resourcer": "1.6.20"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,17 +1,17 @@
{ {
"name": "@nocobase/app", "name": "@nocobase/app",
"version": "1.6.6", "version": "1.6.20",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/database": "1.6.6", "@nocobase/database": "1.6.20",
"@nocobase/preset-nocobase": "1.6.6", "@nocobase/preset-nocobase": "1.6.20",
"@nocobase/server": "1.6.6" "@nocobase/server": "1.6.20"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/client": "1.6.6" "@nocobase/client": "1.6.20"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { mockDatabase } from '@nocobase/database'; import { createMockDatabase, mockDatabase } from '@nocobase/database';
import { uid } from '@nocobase/utils'; import { uid } from '@nocobase/utils';
import axios from 'axios'; import axios from 'axios';
import execa from 'execa'; import execa from 'execa';
@ -64,7 +64,7 @@ const createDatabase = async () => {
if (process.env.DB_DIALECT === 'sqlite') { if (process.env.DB_DIALECT === 'sqlite') {
return 'nocobase'; return 'nocobase';
} }
const db = mockDatabase(); const db = await createMockDatabase();
const name = `d_${uid()}`; const name = `d_${uid()}`;
await db.sequelize.query(`CREATE DATABASE ${name}`); await db.sequelize.query(`CREATE DATABASE ${name}`);
await db.close(); await db.close();

View File

@ -1,16 +1,16 @@
{ {
"name": "@nocobase/auth", "name": "@nocobase/auth",
"version": "1.6.6", "version": "1.6.20",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/actions": "1.6.6", "@nocobase/actions": "1.6.20",
"@nocobase/cache": "1.6.6", "@nocobase/cache": "1.6.20",
"@nocobase/database": "1.6.6", "@nocobase/database": "1.6.20",
"@nocobase/resourcer": "1.6.6", "@nocobase/resourcer": "1.6.20",
"@nocobase/utils": "1.6.6", "@nocobase/utils": "1.6.20",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"jsonwebtoken": "^8.5.1" "jsonwebtoken": "^8.5.1"
}, },

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/build", "name": "@nocobase/build",
"version": "1.6.6", "version": "1.6.20",
"description": "Library build tool based on rollup.", "description": "Library build tool based on rollup.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
@ -17,7 +17,7 @@
"@lerna/project": "4.0.0", "@lerna/project": "4.0.0",
"@rsbuild/plugin-babel": "^1.0.3", "@rsbuild/plugin-babel": "^1.0.3",
"@rsdoctor/rspack-plugin": "^0.4.8", "@rsdoctor/rspack-plugin": "^0.4.8",
"@rspack/core": "1.1.1", "@rspack/core": "1.3.2",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@types/gulp": "^4.0.13", "@types/gulp": "^4.0.13",
"@types/lerna__package": "5.1.0", "@types/lerna__package": "5.1.0",

View File

@ -347,6 +347,7 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou
umdNamedDefine: true, umdNamedDefine: true,
}, },
}, },
amd: {},
resolve: { resolve: {
tsConfig: path.join(process.cwd(), 'tsconfig.json'), tsConfig: path.join(process.cwd(), 'tsconfig.json'),
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.css'], extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.css'],

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/cache", "name": "@nocobase/cache",
"version": "1.6.6", "version": "1.6.20",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/cli", "name": "@nocobase/cli",
"version": "1.6.6", "version": "1.6.20",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./src/index.js", "main": "./src/index.js",
@ -8,7 +8,7 @@
"nocobase": "./bin/index.js" "nocobase": "./bin/index.js"
}, },
"dependencies": { "dependencies": {
"@nocobase/app": "1.6.6", "@nocobase/app": "1.6.20",
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@umijs/utils": "3.5.20", "@umijs/utils": "3.5.20",
"chalk": "^4.1.1", "chalk": "^4.1.1",
@ -25,7 +25,7 @@
"tsx": "^4.19.0" "tsx": "^4.19.0"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/devtools": "1.6.6" "@nocobase/devtools": "1.6.20"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -460,6 +460,16 @@ exports.initEnv = function initEnv() {
process.env.SOCKET_PATH = generateGatewayPath(); process.env.SOCKET_PATH = generateGatewayPath();
fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { force: true, recursive: true }); fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { force: true, recursive: true });
fs.mkdirpSync(process.env.PM2_HOME, { force: true, recursive: true }); fs.mkdirpSync(process.env.PM2_HOME, { force: true, recursive: true });
const pkgs = [
'@nocobase/plugin-multi-app-manager',
'@nocobase/plugin-departments',
'@nocobase/plugin-field-attachment-url',
'@nocobase/plugin-workflow-response-message',
];
for (const pkg of pkgs) {
const pkgDir = resolve(process.cwd(), 'storage/plugins', pkg);
fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { recursive: true, force: true });
}
}; };
exports.generatePlugins = function () { exports.generatePlugins = function () {

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/client", "name": "@nocobase/client",
"version": "1.6.6", "version": "1.6.20",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "lib/index.js", "main": "lib/index.js",
"module": "es/index.mjs", "module": "es/index.mjs",
@ -27,9 +27,9 @@
"@formily/reactive-react": "^2.2.27", "@formily/reactive-react": "^2.2.27",
"@formily/shared": "^2.2.27", "@formily/shared": "^2.2.27",
"@formily/validator": "^2.2.27", "@formily/validator": "^2.2.27",
"@nocobase/evaluators": "1.6.6", "@nocobase/evaluators": "1.6.20",
"@nocobase/sdk": "1.6.6", "@nocobase/sdk": "1.6.20",
"@nocobase/utils": "1.6.6", "@nocobase/utils": "1.6.20",
"ahooks": "^3.7.2", "ahooks": "^3.7.2",
"antd": "5.12.8", "antd": "5.12.8",
"antd-style": "3.7.1", "antd-style": "3.7.1",

View File

@ -74,6 +74,7 @@ export const ACLRolesCheckProvider = (props) => {
url: 'roles:check', url: 'roles:check',
}, },
{ {
manual: !api.auth.token,
onSuccess(data) { onSuccess(data) {
if (!data?.data?.snippets.includes('ui.*')) { if (!data?.data?.snippets.includes('ui.*')) {
setDesignable(false); setDesignable(false);

View File

@ -139,7 +139,19 @@ export class APIClient extends APIClientSDK {
if (typeof error?.response?.data === 'string') { if (typeof error?.response?.data === 'string') {
const tempElement = document.createElement('div'); const tempElement = document.createElement('div');
tempElement.innerHTML = error?.response?.data; tempElement.innerHTML = error?.response?.data;
return [{ message: tempElement.textContent || tempElement.innerText }]; let message = tempElement.textContent || tempElement.innerText;
if (message.includes('Error occurred while trying')) {
message = 'The application may be starting up. Please try again later.';
return [{ code: 'APP_WARNING', message }];
}
if (message.includes('502 Bad Gateway')) {
message = 'The application may be starting up. Please try again later.';
return [{ code: 'APP_WARNING', message }];
}
return [{ message }];
}
if (error?.response?.data?.error) {
return [error?.response?.data?.error];
} }
return ( return (
error?.response?.data?.errors || error?.response?.data?.errors ||

View File

@ -350,23 +350,9 @@ export class Application {
setTimeout(() => resolve(null), 1000); setTimeout(() => resolve(null), 1000);
}); });
} }
const toError = (error) => {
if (typeof error?.response?.data === 'string') {
const tempElement = document.createElement('div');
tempElement.innerHTML = error?.response?.data;
return { message: tempElement.textContent || tempElement.innerText };
}
if (error?.response?.data?.error) {
return error?.response?.data?.error;
}
if (error?.response?.data?.errors?.[0]) {
return error?.response?.data?.errors?.[0];
}
return { message: error?.message };
};
this.error = { this.error = {
code: 'LOAD_ERROR', code: 'LOAD_ERROR',
...toError(error), ...this.apiClient.toErrMessages(error)?.[0],
}; };
console.error(error, this.error); console.error(error, this.error);
} }

View File

@ -11,10 +11,11 @@ import React, { FC } from 'react';
import { MainComponent } from './MainComponent'; import { MainComponent } from './MainComponent';
const Loading: FC = () => <div>Loading...</div>; const Loading: FC = () => <div>Loading...</div>;
const AppError: FC<{ error: Error }> = ({ error }) => { const AppError: FC<{ error: Error & { title?: string } }> = ({ error }) => {
const title = error?.title || 'App Error';
return ( return (
<div> <div>
<div>App Error</div> <div>{title}</div>
{error?.message} {error?.message}
{process.env.__TEST__ && error?.stack} {process.env.__TEST__ && error?.stack}
</div> </div>

View File

@ -63,7 +63,7 @@ export const SchemaInitializerItem = memo(
className: className, className: className,
label: children || compile(title), label: children || compile(title),
onClick: (info) => { onClick: (info) => {
if (info.key !== name) return; if (disabled || info.key !== name) return;
if (closeInitializerMenuWhenClick) { if (closeInitializerMenuWhenClick) {
setVisible?.(false); setVisible?.(false);
} }
@ -73,10 +73,10 @@ export const SchemaInitializerItem = memo(
children: childrenItems, children: childrenItems,
}, },
]; ];
}, [name, style, className, children, title, onClick, icon, childrenItems]); }, [name, disabled, style, className, children, title, onClick, icon, childrenItems]);
if (items && items.length > 0) { if (items && items.length > 0) {
return <SchemaInitializerMenu items={menuItems}></SchemaInitializerMenu>; return <SchemaInitializerMenu disabled={disabled} items={menuItems}></SchemaInitializerMenu>;
} }
return ( return (
<div <div

View File

@ -10,11 +10,11 @@
import { useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import React from 'react'; import React from 'react';
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
import { DatePickerProvider, ActionBarProvider, SchemaComponentOptions } from '../schema-component'; import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
import { ActionBarProvider, DatePickerProvider, SchemaComponentOptions } from '../schema-component';
import { DefaultValueProvider } from '../schema-settings'; import { DefaultValueProvider } from '../schema-settings';
import { CollectOperators } from './CollectOperators'; import { CollectOperators } from './CollectOperators';
import { FormBlockProvider } from './FormBlockProvider'; import { FormBlockProvider } from './FormBlockProvider';
import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => { export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
const filedSchema = useFieldSchema(); const filedSchema = useFieldSchema();
@ -35,7 +35,7 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
}} }}
> >
<DefaultValueProvider isAllowToSetDefaultValue={() => false}> <DefaultValueProvider isAllowToSetDefaultValue={() => false}>
<FormBlockProvider name="filter-form" {...props}></FormBlockProvider> <FormBlockProvider name="filter-form" {...props} confirmBeforeClose={false}></FormBlockProvider>
</DefaultValueProvider> </DefaultValueProvider>
</ActionBarProvider> </ActionBarProvider>
</DatePickerProvider> </DatePickerProvider>

View File

@ -167,7 +167,7 @@ export function useCollectValuesToSubmit() {
if (parsedValue !== null && parsedValue !== undefined) { if (parsedValue !== null && parsedValue !== undefined) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
} }
}); });
@ -338,7 +338,7 @@ export const useAssociationCreateActionProps = () => {
if (parsedValue) { if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
} }
}); });
@ -522,9 +522,11 @@ export const useFilterBlockActionProps = () => {
const { doFilter } = useDoFilter(); const { doFilter } = useDoFilter();
const actionField = useField(); const actionField = useField();
actionField.data = actionField.data || {}; actionField.data = actionField.data || {};
const form = useForm();
return { return {
async onClick() { async onClick() {
await form.submit();
actionField.data.loading = true; actionField.data.loading = true;
await doFilter(); await doFilter();
actionField.data.loading = false; actionField.data.loading = false;
@ -605,7 +607,7 @@ export const useCustomizeUpdateActionProps = () => {
if (parsedValue) { if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
} }
}); });
@ -708,7 +710,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
if (parsedValue) { if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
} }
}); });
@ -930,7 +932,7 @@ export const useUpdateActionProps = () => {
if (parsedValue) { if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
} }
}); });

View File

@ -62,6 +62,12 @@ export class InputFieldInterface extends CollectionFieldInterface {
hasDefaultValue = true; hasDefaultValue = true;
properties = { properties = {
...defaultProps, ...defaultProps,
trim: {
type: 'boolean',
'x-content': '{{t("Automatically remove heading and tailing spaces")}}',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
},
layout: { layout: {
type: 'void', type: 'void',
title: '{{t("Index")}}', title: '{{t("Index")}}',

View File

@ -129,12 +129,12 @@ export const enumType = [
label: '{{t("is")}}', label: '{{t("is")}}',
value: '$eq', value: '$eq',
selected: true, selected: true,
schema: { 'x-component': 'Select' }, schema: { 'x-component': 'Select', 'x-component-props': { mode: null } },
}, },
{ {
label: '{{t("is not")}}', label: '{{t("is not")}}',
value: '$ne', value: '$ne',
schema: { 'x-component': 'Select' }, schema: { 'x-component': 'Select', 'x-component-props': { mode: null } },
}, },
{ {
label: '{{t("is any of")}}', label: '{{t("is any of")}}',

View File

@ -31,6 +31,12 @@ export class TextareaFieldInterface extends CollectionFieldInterface {
titleUsable = true; titleUsable = true;
properties = { properties = {
...defaultProps, ...defaultProps,
trim: {
type: 'boolean',
'x-content': '{{t("Automatically remove heading and tailing spaces")}}',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
},
}; };
schemaInitialize(schema: ISchema, { block }) { schemaInitialize(schema: ISchema, { block }) {
if (['Table', 'Kanban'].includes(block)) { if (['Table', 'Kanban'].includes(block)) {

View File

@ -18,7 +18,14 @@ export interface SelectWithTitleProps {
onChange?: (...args: any[]) => void; onChange?: (...args: any[]) => void;
} }
export function SelectWithTitle({ title, defaultValue, onChange, options, fieldNames }: SelectWithTitleProps) { export function SelectWithTitle({
title,
defaultValue,
onChange,
options,
fieldNames,
...others
}: SelectWithTitleProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const timerRef = useRef<any>(null); const timerRef = useRef<any>(null);
return ( return (
@ -36,6 +43,7 @@ export function SelectWithTitle({ title, defaultValue, onChange, options, fieldN
> >
{title} {title}
<Select <Select
{...others}
open={open} open={open}
data-testid={`select-${title}`} data-testid={`select-${title}`}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}

View File

@ -18,6 +18,7 @@ import { useCollectionFieldUISchema, useIsInNocoBaseRecursionFieldContext } from
import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps'; import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps';
import { useCompile, useComponent } from '../../schema-component'; import { useCompile, useComponent } from '../../schema-component';
import { useIsAllowToSetDefaultValue } from '../../schema-settings/hooks/useIsAllowToSetDefaultValue'; import { useIsAllowToSetDefaultValue } from '../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { isVariable } from '../../variables/utils/isVariable';
import { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider'; import { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider';
type Props = { type Props = {
@ -102,17 +103,43 @@ const CollectionFieldInternalField = (props) => {
const dynamicProps = useDynamicComponentProps(uiSchema?.['x-use-component-props'], props); const dynamicProps = useDynamicComponentProps(uiSchema?.['x-use-component-props'], props);
useEffect(() => { useEffect(() => {
// There seems to be a bug in formily where after setting a field to readPretty, switching to editable, /**
// then back to readPretty, and refreshing the page, the field remains in editable state. The expected state is readPretty. * There seems to be a bug in formily where after setting a field to readPretty, switching to editable,
// This code is meant to fix this issue. * then back to readPretty, and refreshing the page, the field remains in editable state. The expected state is readPretty.
* This code is meant to fix this issue.
*/
if (fieldSchema['x-read-pretty'] === true && !field.readPretty) { if (fieldSchema['x-read-pretty'] === true && !field.readPretty) {
field.readPretty = true; field.readPretty = true;
} }
/**
* This solves the issue: After creating a form and setting a field to "read-only", the field remains editable when refreshing the page and reopening the dialog.
*
* Note: This might be a bug in Formily
* When both x-disabled and x-read-pretty exist in the Schema:
* - If x-disabled appears before x-read-pretty in the Schema JSON, the disabled state becomes ineffective
* - The reason is that during field instance initialization, field.disabled is set before field.readPretty, which causes the pattern value to be changed to 'editable'
* - This issue is related to the order of JSON fields, which might return different orders in different environments (databases), thus making the issue inconsistent to reproduce
*
* Reference to Formily source code:
* 1. Setting readPretty may cause pattern to be changed to 'editable': https://github.com/alibaba/formily/blob/d4bb96c40e7918210b1bd7d57b8fadee0cfe4b26/packages/core/src/models/BaseField.ts#L208-L224
* 2. The execution order of the each method depends on the order of JSON fields: https://github.com/alibaba/formily/blob/123d536b6076196e00b4e02ee160d72480359f54/packages/json-schema/src/schema.ts#L486-L519
*/
if (fieldSchema['x-disabled'] === true) {
field.disabled = true;
}
}, [field, fieldSchema]); }, [field, fieldSchema]);
if (!uiSchema) return null; if (!uiSchema) return null;
return <Component {...props} {...dynamicProps} />; const mergedProps = { ...props, ...dynamicProps };
// Prevent displaying the variable string first, then the variable value
if (isVariable(mergedProps.value) && mergedProps.value === fieldSchema.default) {
mergedProps.value = undefined;
}
return <Component {...mergedProps} />;
}; };
export const CollectionField = connect((props) => { export const CollectionField = connect((props) => {

View File

@ -48,6 +48,7 @@ interface INocoBaseRecursionFieldProps extends IRecursionFieldProps {
* Whether to use Formily Field class - performance will be reduced but provides better compatibility with Formily * Whether to use Formily Field class - performance will be reduced but provides better compatibility with Formily
*/ */
isUseFormilyField?: boolean; isUseFormilyField?: boolean;
parentSchema?: Schema;
} }
const CollectionFieldUISchemaContext = React.createContext<CollectionFieldOptions>({}); const CollectionFieldUISchemaContext = React.createContext<CollectionFieldOptions>({});
@ -266,6 +267,7 @@ export const NocoBaseRecursionField: ReactFC<INocoBaseRecursionFieldProps> = Rea
values, values,
isUseFormilyField = true, isUseFormilyField = true,
uiSchema, uiSchema,
parentSchema,
} = props; } = props;
const basePath = useBasePath(props); const basePath = useBasePath(props);
const newFieldSchemaRef = useRef(null); const newFieldSchemaRef = useRef(null);
@ -279,6 +281,14 @@ export const NocoBaseRecursionField: ReactFC<INocoBaseRecursionFieldProps> = Rea
const fieldSchema: Schema = newFieldSchemaRef.current || oldFieldSchema; const fieldSchema: Schema = newFieldSchemaRef.current || oldFieldSchema;
// Establish connection with the Schema tree
if (!fieldSchema.parent && parentSchema) {
fieldSchema.parent = parentSchema;
if (!fieldSchema.parent?.properties?.[fieldSchema.name] && fieldSchema.name) {
_.set(fieldSchema.parent, `properties.${fieldSchema.name}`, fieldSchema);
}
}
const refresh = useCallback(() => { const refresh = useCallback(() => {
const parent = fieldSchema.parent; const parent = fieldSchema.parent;
newFieldSchemaRef.current = new Schema(fieldSchema.toJSON(), parent); newFieldSchemaRef.current = new Schema(fieldSchema.toJSON(), parent);

View File

@ -884,5 +884,7 @@
"If selected, the page will display Tab pages.": "Wenn ausgewählt, zeigt die Seite Tab-Seiten an.", "If selected, the page will display Tab pages.": "Wenn ausgewählt, zeigt die Seite Tab-Seiten an.",
"If selected, the route will be displayed in the menu.": "Wenn ausgewählt, wird die Route im Menü angezeigt.", "If selected, the route will be displayed in the menu.": "Wenn ausgewählt, wird die Route im Menü angezeigt.",
"Are you sure you want to hide this tab?": "Sind Sie sicher, dass Sie diesen Tab ausblenden möchten?", "Are you sure you want to hide this tab?": "Sind Sie sicher, dass Sie diesen Tab ausblenden möchten?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Nach dem Ausblenden wird dieser Tab nicht mehr in der Tableiste angezeigt. Um ihn wieder anzuzeigen, müssen Sie zur Routenverwaltungsseite gehen, um ihn einzustellen." "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Nach dem Ausblenden wird dieser Tab nicht mehr in der Tableiste angezeigt. Um ihn wieder anzuzeigen, müssen Sie zur Routenverwaltungsseite gehen, um ihn einzustellen.",
"No pages yet, please configure first": "Noch keine Seiten, bitte zuerst konfigurieren",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Klicken Sie auf das \"UI-Editor\"-Symbol in der oberen rechten Ecke, um den UI-Editor-Modus zu betreten"
} }

View File

@ -884,5 +884,7 @@
"If selected, the page will display Tab pages.": "If selected, the page will display Tab pages.", "If selected, the page will display Tab pages.": "If selected, the page will display Tab pages.",
"If selected, the route will be displayed in the menu.": "If selected, the route will be displayed in the menu.", "If selected, the route will be displayed in the menu.": "If selected, the route will be displayed in the menu.",
"Are you sure you want to hide this tab?": "Are you sure you want to hide this tab?", "Are you sure you want to hide this tab?": "Are you sure you want to hide this tab?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it." "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.",
"No pages yet, please configure first": "No pages yet, please configure first",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode"
} }

View File

@ -801,5 +801,7 @@
"If selected, the page will display Tab pages.": "Si se selecciona, la página mostrará páginas de pestañas.", "If selected, the page will display Tab pages.": "Si se selecciona, la página mostrará páginas de pestañas.",
"If selected, the route will be displayed in the menu.": "Si se selecciona, la ruta se mostrará en el menú.", "If selected, the route will be displayed in the menu.": "Si se selecciona, la ruta se mostrará en el menú.",
"Are you sure you want to hide this tab?": "¿Estás seguro de que quieres ocultar esta pestaña?", "Are you sure you want to hide this tab?": "¿Estás seguro de que quieres ocultar esta pestaña?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Después de ocultar, esta pestaña ya no aparecerá en la barra de pestañas. Para mostrarla de nuevo, deberás ir a la página de gestión de rutas para configurarla." "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Después de ocultar, esta pestaña ya no aparecerá en la barra de pestañas. Para mostrarla de nuevo, deberás ir a la página de gestión de rutas para configurarla.",
"No pages yet, please configure first": "Aún no hay páginas, por favor configura primero",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Haga clic en el icono \"Editor de UI\" en la esquina superior derecha para entrar en el modo de Editor de UI."
} }

View File

@ -821,5 +821,7 @@
"If selected, the page will display Tab pages.": "Si sélectionné, la page affichera des onglets.", "If selected, the page will display Tab pages.": "Si sélectionné, la page affichera des onglets.",
"If selected, the route will be displayed in the menu.": "Si sélectionné, la route sera affichée dans le menu.", "If selected, the route will be displayed in the menu.": "Si sélectionné, la route sera affichée dans le menu.",
"Are you sure you want to hide this tab?": "Êtes-vous sûr de vouloir masquer cet onglet ?", "Are you sure you want to hide this tab?": "Êtes-vous sûr de vouloir masquer cet onglet ?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Après avoir masqué, cette tab ne sera plus affichée dans la barre de tab. Pour la montrer à nouveau, vous devez vous rendre sur la page de gestion des routes pour la configurer." "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Après avoir masqué, cette tab ne sera plus affichée dans la barre de tab. Pour la montrer à nouveau, vous devez vous rendre sur la page de gestion des routes pour la configurer.",
"No pages yet, please configure first": "Pas encore de pages, veuillez configurer d'abord",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur"
} }

File diff suppressed because it is too large Load Diff

View File

@ -1039,5 +1039,7 @@
"If selected, the page will display Tab pages.": "選択されている場合、ページはタブページを表示します。", "If selected, the page will display Tab pages.": "選択されている場合、ページはタブページを表示します。",
"If selected, the route will be displayed in the menu.": "選択されている場合、ルートはメニューに表示されます。", "If selected, the route will be displayed in the menu.": "選択されている場合、ルートはメニューに表示されます。",
"Are you sure you want to hide this tab?": "このタブを非表示にしますか?", "Are you sure you want to hide this tab?": "このタブを非表示にしますか?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "非表示にすると、このタブはタブバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。" "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "非表示にすると、このタブはタブバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。",
"No pages yet, please configure first": "まだページがありません。最初に設定してください",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "ユーザーインターフェースエディターモードに入るには、右上隅の「UIエディタ」アイコンをクリックしてください"
} }

View File

@ -912,5 +912,7 @@
"If selected, the page will display Tab pages.": "선택되면 페이지는 탭 페이지를 표시합니다.", "If selected, the page will display Tab pages.": "선택되면 페이지는 탭 페이지를 표시합니다.",
"If selected, the route will be displayed in the menu.": "선택되면 라우트는 메뉴에 표시됩니다.", "If selected, the route will be displayed in the menu.": "선택되면 라우트는 메뉴에 표시됩니다.",
"Are you sure you want to hide this tab?": "이 탭을 숨기시겠습니까?", "Are you sure you want to hide this tab?": "이 탭을 숨기시겠습니까?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "숨기면 이 탭은 탭 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다." "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "숨기면 이 탭은 탭 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다.",
"No pages yet, please configure first": "아직 페이지가 없습니다. 먼저 설정하십시오",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "사용자 인터페이스 편집기 모드에 들어가려면 오른쪽 상단의 \"UI 편집기\" 아이콘을 클릭하십시오"
} }

View File

@ -778,5 +778,7 @@
"If selected, the page will display Tab pages.": "Se selecionado, a página exibirá páginas de abas.", "If selected, the page will display Tab pages.": "Se selecionado, a página exibirá páginas de abas.",
"If selected, the route will be displayed in the menu.": "Se selecionado, a rota será exibida no menu.", "If selected, the route will be displayed in the menu.": "Se selecionado, a rota será exibida no menu.",
"Are you sure you want to hide this tab?": "Tem certeza de que deseja ocultar esta guia?", "Are you sure you want to hide this tab?": "Tem certeza de que deseja ocultar esta guia?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Depois de ocultar, esta guia não aparecerá mais na barra de guias. Para mostrá-la novamente, você precisa ir à página de gerenciamento de rotas para configurá-la." "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Depois de ocultar, esta guia não aparecerá mais na barra de guias. Para mostrá-la novamente, você precisa ir à página de gerenciamento de rotas para configurá-la.",
"No pages yet, please configure first": "Ainda não há páginas, por favor configure primeiro",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur"
} }

View File

@ -607,5 +607,7 @@
"If selected, the page will display Tab pages.": "Если выбран, страница будет отображать страницы с вкладками.", "If selected, the page will display Tab pages.": "Если выбран, страница будет отображать страницы с вкладками.",
"If selected, the route will be displayed in the menu.": "Если выбран, маршрут будет отображаться в меню.", "If selected, the route will be displayed in the menu.": "Если выбран, маршрут будет отображаться в меню.",
"Are you sure you want to hide this tab?": "Вы уверены, что хотите скрыть эту вкладку?", "Are you sure you want to hide this tab?": "Вы уверены, что хотите скрыть эту вкладку?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "После скрытия этой вкладки она больше не будет отображаться во вкладке. Чтобы снова отобразить ее, вам нужно будет перейти на страницу управления маршрутами, чтобы установить ее." "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "После скрытия этой вкладки она больше не будет отображаться во вкладке. Чтобы снова отобразить ее, вам нужно будет перейти на страницу управления маршрутами, чтобы установить ее.",
"No pages yet, please configure first": "Пока нет страниц, пожалуйста, настройте сначала",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Нажмите на значок \"Редактор пользовательского интерфейса\" в правом верхнем углу, чтобы войти в режим редактора пользовательского интерфейса"
} }

View File

@ -605,5 +605,7 @@
"If selected, the page will display Tab pages.": "Seçildiğinde, sayfa Tab sayfalarını görüntüleyecektir.", "If selected, the page will display Tab pages.": "Seçildiğinde, sayfa Tab sayfalarını görüntüleyecektir.",
"If selected, the route will be displayed in the menu.": "Seçildiğinde, yol menüde görüntülenecektir.", "If selected, the route will be displayed in the menu.": "Seçildiğinde, yol menüde görüntülenecektir.",
"Are you sure you want to hide this tab?": "Bu sekmeyi gizlemek istediğinizden emin misiniz?", "Are you sure you want to hide this tab?": "Bu sekmeyi gizlemek istediğinizden emin misiniz?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Gizlendikten sonra, bu sekme artık sekme çubuğunda görünmeyecek. Onu tekrar göstermek için, rotayı yönetim sayfasına gidip ayarlamanız gerekiyor." "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Gizlendikten sonra, bu sekme artık sekme çubuğunda görünmeyecek. Onu tekrar göstermek için, rotayı yönetim sayfasına gidip ayarlamanız gerekiyor.",
"No pages yet, please configure first": "Henüz sayfa yok, lütfen önce yapılandırın",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Kullanıcı arayüzü düzenleyici moduna girmek için sağ üst köşedeki \"Kullanıcı Arayüzü Düzenleyici\" simgesine tıklayın"
} }

View File

@ -821,5 +821,7 @@
"If selected, the page will display Tab pages.": "Якщо вибрано, сторінка відобразить сторінки з вкладками.", "If selected, the page will display Tab pages.": "Якщо вибрано, сторінка відобразить сторінки з вкладками.",
"If selected, the route will be displayed in the menu.": "Якщо вибрано, маршрут буде відображений в меню.", "If selected, the route will be displayed in the menu.": "Якщо вибрано, маршрут буде відображений в меню.",
"Are you sure you want to hide this tab?": "Ви впевнені, що хочете приховати цю вкладку?", "Are you sure you want to hide this tab?": "Ви впевнені, що хочете приховати цю вкладку?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Після приховування цієї вкладки вона більше не з'явиться в панелі вкладок. Щоб знову показати її, вам потрібно перейти на сторінку керування маршрутами, щоб налаштувати її." "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Після приховування цієї вкладки вона більше не з'явиться в панелі вкладок. Щоб знову показати її, вам потрібно перейти на сторінку керування маршрутами, щоб налаштувати її.",
"No pages yet, please configure first": "Ще немає сторінок, будь ласка, спочатку налаштуйте",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Натисніть на значок \"Редактор користувацького інтерфейсу\" в правому верхньому куті, щоб увійти в режим редактора користувацького інтерфейсу."
} }

View File

@ -258,6 +258,7 @@
"Parent collection fields": "父表字段", "Parent collection fields": "父表字段",
"Basic": "基本类型", "Basic": "基本类型",
"Single line text": "单行文本", "Single line text": "单行文本",
"Automatically remove heading and tailing spaces": "自动去除首尾空白字符",
"Long text": "多行文本", "Long text": "多行文本",
"Phone": "手机号码", "Phone": "手机号码",
"Email": "电子邮箱", "Email": "电子邮箱",
@ -1080,5 +1081,7 @@
"If selected, the page will display Tab pages.": "如果选中,该页面将显示标签页。", "If selected, the page will display Tab pages.": "如果选中,该页面将显示标签页。",
"If selected, the route will be displayed in the menu.": "如果选中,该路由将显示在菜单中。", "If selected, the route will be displayed in the menu.": "如果选中,该路由将显示在菜单中。",
"Are you sure you want to hide this tab?": "你确定要隐藏该标签页吗?", "Are you sure you want to hide this tab?": "你确定要隐藏该标签页吗?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隐藏后,该标签将不再显示在标签栏中。要想再次显示它,你需要到路由管理页面进行设置。" "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隐藏后,该标签将不再显示在标签栏中。要想再次显示它,你需要到路由管理页面进行设置。",
"No pages yet, please configure first": "暂无页面,请先配置",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "点击右上角的“界面配置”图标,进入界面配置模式"
} }

View File

@ -912,6 +912,7 @@
"If selected, the page will display Tab pages.": "如果選中,該頁面將顯示標籤頁。", "If selected, the page will display Tab pages.": "如果選中,該頁面將顯示標籤頁。",
"If selected, the route will be displayed in the menu.": "如果選中,該路由將顯示在菜單中。", "If selected, the route will be displayed in the menu.": "如果選中,該路由將顯示在菜單中。",
"Are you sure you want to hide this tab?": "你確定要隱藏這個標籤嗎?", "Are you sure you want to hide this tab?": "你確定要隱藏這個標籤嗎?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隱藏後,這個標籤將不再出現在標籤欄中。要再次顯示它,你需要到路由管理頁面進行設置。" "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隱藏後,這個標籤將不再出現在標籤欄中。要再次顯示它,你需要到路由管理頁面進行設置。",
"No pages yet, please configure first": "尚未配置頁面,請先配置",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "點擊右上角的 \"介面設定\" 圖示進入介面設定模式"
} }

View File

@ -40,6 +40,7 @@ test.describe('where list block can be added', () => {
await page.getByLabel('schema-initializer-Grid-').nth(1).hover(); await page.getByLabel('schema-initializer-Grid-').nth(1).hover();
await page.getByRole('menuitem', { name: 'Role name' }).click(); await page.getByRole('menuitem', { name: 'Role name' }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);
await page.reload();
await expect(page.getByLabel('block-item-CollectionField-').getByText('Root')).toBeVisible(); await expect(page.getByLabel('block-item-CollectionField-').getByText('Root')).toBeVisible();
await expect(page.getByLabel('block-item-CollectionField-').getByText('Admin')).toBeVisible(); await expect(page.getByLabel('block-item-CollectionField-').getByText('Admin')).toBeVisible();
await expect(page.getByLabel('block-item-CollectionField-').getByText('Member')).toBeVisible(); await expect(page.getByLabel('block-item-CollectionField-').getByText('Member')).toBeVisible();

View File

@ -94,9 +94,13 @@ export const FilterCollectionFieldInternalField: React.FC = (props: Props) => {
// @ts-ignore // @ts-ignore
field.dataSource = uiSchema.enum; field.dataSource = uiSchema.enum;
const originalProps = const originalProps =
compile({ ...(operator?.schema?.['x-component-props'] || {}), ...(uiSchema['x-component-props'] || {}) }) || {}; compile({
...(operator?.schema?.['x-component-props'] || {}),
...(uiSchema['x-component-props'] || {}),
...(fieldSchema?.['x-component-props'] || {}),
}) || {};
field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {}); field.componentProps = merge(field.componentProps || {}, originalProps, dynamicProps || {});
}, [uiSchemaOrigin]); }, [uiSchemaOrigin]);
if (!uiSchemaOrigin) return null; if (!uiSchemaOrigin) return null;

View File

@ -8,6 +8,7 @@
*/ */
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import _ from 'lodash';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useBlockContext, useOpenModeContext } from '../../../../'; import { useBlockContext, useOpenModeContext } from '../../../../';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
@ -45,11 +46,12 @@ export const ellipsisSettingsItem: SchemaSettingsItemType = {
tableFieldInstanceList.forEach((fieldInstance) => { tableFieldInstanceList.forEach((fieldInstance) => {
fieldInstance.componentProps.ellipsis = checked; fieldInstance.componentProps.ellipsis = checked;
}); });
schema['x-component-props']['ellipsis'] = checked;
} else { } else {
formField.componentProps.ellipsis = checked; formField.componentProps.ellipsis = checked;
} }
_.set(schema, 'x-component-props.ellipsis', checked);
await dn.emit('patch', { await dn.emit('patch', {
schema: { schema: {
'x-uid': schema['x-uid'], 'x-uid': schema['x-uid'],

View File

@ -126,6 +126,10 @@ export const getAllowMultiple = (params?: { title: string }) => {
return { return {
name: 'allowMultiple', name: 'allowMultiple',
type: 'switch', type: 'switch',
useVisible() {
const isAssociationField = useIsAssociationField();
return isAssociationField;
},
useComponentProps() { useComponentProps() {
const { t } = useTranslation(); const { t } = useTranslation();
const field = useField<Field>(); const field = useField<Field>();

View File

@ -14,6 +14,14 @@ import React, { useCallback, useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Router } from 'react-router-dom'; import { Router } from 'react-router-dom';
import { SchemaInitializerItem } from '../../application'; import { SchemaInitializerItem } from '../../application';
import {
CollectionManagerProvider,
useCollectionManager,
} from '../../data-source/collection/CollectionManagerProvider';
import {
DataSourceManagerProvider,
useDataSourceManager,
} from '../../data-source/data-source/DataSourceManagerProvider';
import { useGlobalTheme } from '../../global-theme'; import { useGlobalTheme } from '../../global-theme';
import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema'; import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
import { import {
@ -34,6 +42,8 @@ export const LinkMenuItem = () => {
const { urlSchema, paramsSchema } = useURLAndHTMLSchema(); const { urlSchema, paramsSchema } = useURLAndHTMLSchema();
const parentRoute = useParentRoute(); const parentRoute = useParentRoute();
const { createRoute } = useNocoBaseRoutes(); const { createRoute } = useNocoBaseRoutes();
const dm = useDataSourceManager();
const cm = useCollectionManager();
const handleClick = useCallback(async () => { const handleClick = useCallback(async () => {
const values = await FormDialog( const values = await FormDialog(
@ -41,31 +51,35 @@ export const LinkMenuItem = () => {
() => { () => {
const history = createMemoryHistory(); const history = createMemoryHistory();
return ( return (
<Router location={history.location} navigator={history}> <DataSourceManagerProvider dataSourceManager={dm}>
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}> <CollectionManagerProvider instance={cm} dataSource={cm?.dataSource?.key}>
<FormLayout layout={'vertical'}> <Router location={history.location} navigator={history}>
<SchemaComponent <SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
schema={{ <FormLayout layout={'vertical'}>
properties: { <SchemaComponent
title: { schema={{
title: t('Menu item title'), properties: {
required: true, title: {
'x-component': 'Input', title: t('Menu item title'),
'x-decorator': 'FormItem', required: true,
}, 'x-component': 'Input',
icon: { 'x-decorator': 'FormItem',
title: t('Icon'), },
'x-component': 'IconPicker', icon: {
'x-decorator': 'FormItem', title: t('Icon'),
}, 'x-component': 'IconPicker',
href: urlSchema, 'x-decorator': 'FormItem',
params: paramsSchema, },
}, href: urlSchema,
}} params: paramsSchema,
/> },
</FormLayout> }}
</SchemaComponentOptions> />
</Router> </FormLayout>
</SchemaComponentOptions>
</Router>
</CollectionManagerProvider>
</DataSourceManagerProvider>
); );
}, },
theme, theme,

View File

@ -74,9 +74,9 @@ const useErrorProps = (app: Application, error: any) => {
} }
}; };
const AppError: FC<{ error: Error; app: Application }> = observer( const AppError: FC<{ error: Error & { title?: string }; app: Application }> = observer(
({ app, error }) => { ({ app, error }) => {
const props = useErrorProps(app, error); const props = getProps(app);
return ( return (
<div> <div>
<Result <Result
@ -87,8 +87,9 @@ const AppError: FC<{ error: Error; app: Application }> = observer(
transform: translate(0, -50%); transform: translate(0, -50%);
`} `}
status="error" status="error"
title={app.i18n.t('App error')} title={error?.title || app.i18n.t('App error', { ns: 'client' })}
subTitle={app.i18n.t(error?.message)} subTitle={app.i18n.t(error?.message)}
{...props}
extra={[ extra={[
<Button type="primary" key="try" onClick={() => window.location.reload()}> <Button type="primary" key="try" onClick={() => window.location.reload()}>
{app.i18n.t('Try again')} {app.i18n.t('Try again')}
@ -124,6 +125,14 @@ const getProps = (app: Application) => {
}; };
} }
if (app.error.code === 'APP_WARNING') {
return {
status: 'warning',
title: 'App warning',
subTitle: app.error?.message,
};
}
if (app.error.code === 'APP_INITIALIZING') { if (app.error.code === 'APP_INITIALIZING') {
return { return {
status: 'info', status: 'info',

View File

@ -0,0 +1,212 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { findFirstPageRoute, NocoBaseDesktopRouteType } from '..';
import { NocoBaseDesktopRoute } from '../convertRoutesToSchema';
describe('findFirstPageRoute', () => {
// 基本测试:空路由数组
it('should return undefined for empty routes array', () => {
const result = findFirstPageRoute([]);
expect(result).toBeUndefined();
});
// 基本测试undefined 路由数组
it('should return undefined for undefined routes', () => {
const result = findFirstPageRoute(undefined);
expect(result).toBeUndefined();
});
// 测试:只有一个页面路由
it('should find the first page route when there is only one page', () => {
const routes: NocoBaseDesktopRoute[] = [
{
id: 1,
schemaUid: 'page1',
type: NocoBaseDesktopRouteType.page,
title: 'Page 1',
},
];
const result = findFirstPageRoute(routes);
expect(result).toEqual(routes[0]);
});
// 测试:多个页面路由
it('should find the first page route when there are multiple pages', () => {
const routes: NocoBaseDesktopRoute[] = [
{
id: 1,
schemaUid: 'page1',
type: NocoBaseDesktopRouteType.page,
title: 'Page 1',
},
{
id: 2,
schemaUid: 'page2',
type: NocoBaseDesktopRouteType.page,
title: 'Page 2',
},
];
const result = findFirstPageRoute(routes);
expect(result).toEqual(routes[0]);
});
// 测试:不同类型的路由混合
it('should find the first page route among mixed route types', () => {
const routes: NocoBaseDesktopRoute[] = [
{
id: 1,
schemaUid: 'link1',
type: NocoBaseDesktopRouteType.link,
title: 'Link 1',
},
{
id: 2,
schemaUid: 'page1',
type: NocoBaseDesktopRouteType.page,
title: 'Page 1',
},
];
const result = findFirstPageRoute(routes);
expect(result).toEqual(routes[1]);
});
// 测试:隐藏的菜单项
it('should ignore hidden menu items', () => {
const routes: NocoBaseDesktopRoute[] = [
{
id: 1,
schemaUid: 'page1',
type: NocoBaseDesktopRouteType.page,
title: 'Page 1',
hideInMenu: true,
},
{
id: 2,
schemaUid: 'page2',
type: NocoBaseDesktopRouteType.page,
title: 'Page 2',
},
];
const result = findFirstPageRoute(routes);
expect(result).toEqual(routes[1]);
});
// 测试:嵌套路由
it('should find page route in nested group', () => {
const routes: NocoBaseDesktopRoute[] = [
{
id: 1,
type: NocoBaseDesktopRouteType.group,
title: 'Group 1',
children: [
{
id: 11,
schemaUid: 'page1',
type: NocoBaseDesktopRouteType.page,
title: 'Page 1',
},
],
},
];
const result = findFirstPageRoute(routes);
expect(result).toEqual(routes[0].children[0]);
});
// 测试:多层嵌套路由
it('should find page route in deeply nested groups', () => {
const routes: NocoBaseDesktopRoute[] = [
{
id: 1,
type: NocoBaseDesktopRouteType.group,
title: 'Group 1',
children: [
{
id: 11,
type: NocoBaseDesktopRouteType.group,
title: 'Group 1-1',
children: [
{
id: 111,
schemaUid: 'page1',
type: NocoBaseDesktopRouteType.page,
title: 'Page 1',
},
],
},
],
},
];
const result = findFirstPageRoute(routes);
expect(result).toEqual(routes[0].children[0].children[0]);
});
// 测试:复杂路由结构
it('should find the first visible page in a complex route structure', () => {
const routes: NocoBaseDesktopRoute[] = [
{
id: 1,
type: NocoBaseDesktopRouteType.group,
title: 'Group 1',
hideInMenu: true,
children: [
{
id: 11,
schemaUid: 'page1',
type: NocoBaseDesktopRouteType.page,
title: 'Page 1',
},
],
},
{
id: 2,
type: NocoBaseDesktopRouteType.group,
title: 'Group 2',
children: [
{
id: 21,
schemaUid: 'page2',
type: NocoBaseDesktopRouteType.page,
title: 'Page 2',
},
],
},
];
const result = findFirstPageRoute(routes);
expect(result).toEqual(routes[1].children[0]);
});
// 测试:空组
it('should skip empty groups and find page in next group', () => {
const routes: NocoBaseDesktopRoute[] = [
{
id: 1,
type: NocoBaseDesktopRouteType.group,
title: 'Empty Group',
children: [],
},
{
id: 2,
schemaUid: 'page1',
type: NocoBaseDesktopRouteType.page,
title: 'Page 1',
},
];
const result = findFirstPageRoute(routes);
expect(result).toEqual(routes[1]);
});
});

View File

@ -7,13 +7,14 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { EllipsisOutlined } from '@ant-design/icons'; import { EllipsisOutlined, HighlightOutlined } from '@ant-design/icons';
import ProLayout, { RouteContext, RouteContextType } from '@ant-design/pro-layout'; import ProLayout, { RouteContext, RouteContextType } from '@ant-design/pro-layout';
import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header'; import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Popover, Tooltip } from 'antd'; import { Popover, Result, Tooltip } from 'antd';
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { useTranslation } from 'react-i18next';
import { Link, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom'; import { Link, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { import {
ACLRolesCheckProvider, ACLRolesCheckProvider,
@ -197,12 +198,34 @@ const pageContentStyle: React.CSSProperties = {
overflowY: 'auto', overflowY: 'auto',
}; };
const ShowTipWhenNoPages = () => {
const { allAccessRoutes } = useAllAccessDesktopRoutes();
const { designable } = useDesignable();
const { token } = useToken();
const { t } = useTranslation();
const location = useLocation();
// Check if there are any pages
if (allAccessRoutes.length === 0 && !designable && ['/admin', '/admin/'].includes(location.pathname)) {
return (
<Result
icon={<HighlightOutlined style={{ fontSize: '8em', color: token.colorText }} />}
title={t('No pages yet, please configure first')}
subTitle={t(`Click the "UI Editor" icon in the upper right corner to enter the UI Editor mode`)}
/>
);
}
return null;
};
export const LayoutContent = () => { export const LayoutContent = () => {
/* Use the "nb-subpages-slot-without-header-and-side" class name to locate the position of the subpages */ /* Use the "nb-subpages-slot-without-header-and-side" class name to locate the position of the subpages */
return ( return (
<div className={`${layoutContentClass} nb-subpages-slot-without-header-and-side`}> <div className={`${layoutContentClass} nb-subpages-slot-without-header-and-side`}>
<div style={pageContentStyle}> <div style={pageContentStyle}>
<Outlet /> <Outlet />
<ShowTipWhenNoPages />
</div> </div>
</div> </div>
); );
@ -588,27 +611,11 @@ export const InternalAdminLayout = () => {
); );
}; };
function getDefaultPageUid(routes: NocoBaseDesktopRoute[]) {
// Find the first route of type "page"
for (const route of routes) {
if (route.type === NocoBaseDesktopRouteType.page) {
return route.schemaUid;
}
if (route.children?.length) {
const result = getDefaultPageUid(route.children);
if (result) {
return result;
}
}
}
}
const NavigateToDefaultPage: FC = (props) => { const NavigateToDefaultPage: FC = (props) => {
const { allAccessRoutes } = useAllAccessDesktopRoutes(); const { allAccessRoutes } = useAllAccessDesktopRoutes();
const location = useLocationNoUpdate(); const location = useLocationNoUpdate();
const defaultPageUid = getDefaultPageUid(allAccessRoutes); const defaultPageUid = findFirstPageRoute(allAccessRoutes)?.schemaUid;
return ( return (
<> <>
@ -718,36 +725,6 @@ export function findRouteBySchemaUid(schemaUid: string, treeArray: any[]) {
return null; return null;
} }
const MenuItemIcon: FC<{ icon: string; title: string }> = (props) => {
const { inHeader } = useContext(headerContext);
return (
<RouteContext.Consumer>
{(value: RouteContextType) => {
const { collapsed } = value;
if (collapsed && !inHeader) {
return props.icon ? (
<Icon type={props.icon} />
) : (
<span
style={{
display: 'inline-block',
width: '100%',
textAlign: 'center',
}}
>
{props.title.charAt(0)}
</span>
);
}
return props.icon ? <Icon type={props.icon} /> : null;
}}
</RouteContext.Consumer>
);
};
const MenuDesignerButton: FC<{ testId: string }> = (props) => { const MenuDesignerButton: FC<{ testId: string }> = (props) => {
const { render: renderInitializer } = useSchemaInitializerRender(menuItemInitializer); const { render: renderInitializer } = useSchemaInitializerRender(menuItemInitializer);
@ -869,16 +846,17 @@ function findRouteById(id: string, treeArray: any[]) {
return null; return null;
} }
function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) { export function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) {
if (!routes) return; if (!routes) return;
for (const route of routes) { for (const route of routes.filter((item) => !item.hideInMenu)) {
if (route.type === NocoBaseDesktopRouteType.page) { if (route.type === NocoBaseDesktopRouteType.page) {
return route; return route;
} }
if (route.children?.length) { if (route.type === NocoBaseDesktopRouteType.group && route.children?.length) {
return findFirstPageRoute(route.children); const result = findFirstPageRoute(route.children);
if (result) return result;
} }
} }
} }

View File

@ -17,7 +17,12 @@ import { ComposedAction } from './types';
export const ActionLink: ComposedAction = withDynamicSchemaProps( export const ActionLink: ComposedAction = withDynamicSchemaProps(
observer((props: any) => { observer((props: any) => {
return ( return (
<Action {...props} component={props.component || 'a'} className={classnames('nb-action-link', props.className)} /> <Action
{...props}
component={props.component || 'a'}
className={classnames('nb-action-link', props.className)}
isLink
/>
); );
}), }),
{ displayName: 'ActionLink' }, { displayName: 'ActionLink' },

View File

@ -9,7 +9,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { observer, useField, useFieldSchema } from '@formily/react'; import { observer, useField, useFieldSchema } from '@formily/react';
import { Modal, ModalProps } from 'antd'; import { Modal, ModalProps, Skeleton } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { FC, startTransition, useEffect, useState } from 'react'; import React, { FC, startTransition, useEffect, useState } from 'react';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
@ -53,7 +53,6 @@ const ActionModalContent: FC<{ footerNodeName: string; field: any; schema: any }
if (!deferredVisible) { if (!deferredVisible) {
return null; return null;
} }
return ( return (
<NocoBaseRecursionField <NocoBaseRecursionField
basePath={field.address} basePath={field.address}
@ -67,6 +66,19 @@ const ActionModalContent: FC<{ footerNodeName: string; field: any; schema: any }
}, },
); );
export function useDelayedVisible(visible: boolean, delay = 200) {
const [ready, setReady] = useState(false);
useEffect(() => {
if (visible) {
const timer = setTimeout(() => setReady(true), delay);
return () => clearTimeout(timer);
} else {
setReady(false);
}
}, [visible]);
return ready;
}
export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = observer( export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = observer(
(props) => { (props) => {
const { footerNodeName = 'Action.Modal.Footer', width, zIndex: _zIndex, ...others } = props; const { footerNodeName = 'Action.Modal.Footer', width, zIndex: _zIndex, ...others } = props;
@ -90,6 +102,7 @@ export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = obse
} }
const zIndex = getZIndex('modal', _zIndex || parentZIndex, props.level || 0); const zIndex = getZIndex('modal', _zIndex || parentZIndex, props.level || 0);
const ready = useDelayedVisible(visible, 200); // 200ms 与 Modal 动画时间一致
return ( return (
<ActionContextNoRerender> <ActionContextNoRerender>
@ -154,7 +167,11 @@ export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = obse
) )
} }
> >
<ActionModalContent footerNodeName={footerNodeName} field={field} schema={schema} /> {ready ? (
<ActionModalContent footerNodeName={footerNodeName} field={field} schema={schema} />
) : (
<Skeleton active paragraph={{ rows: 6 }} />
)}
</Modal> </Modal>
</TabsContextProvider> </TabsContextProvider>
</zIndexContext.Provider> </zIndexContext.Provider>

View File

@ -247,7 +247,6 @@ const InternalAction: React.FC<InternalActionProps> = observer(function Com(prop
const aclCtx = useACLActionParamsContext(); const aclCtx = useACLActionParamsContext();
const { run, element, disabled: disableAction } = useAction?.(actionCallback) || ({} as any); const { run, element, disabled: disableAction } = useAction?.(actionCallback) || ({} as any);
const disabled = form.disabled || field.disabled || field.data?.disabled || propsDisabled || disableAction; const disabled = form.disabled || field.disabled || field.data?.disabled || propsDisabled || disableAction;
const buttonStyle = useMemo(() => { const buttonStyle = useMemo(() => {
return { return {
...style, ...style,
@ -538,6 +537,7 @@ const RenderButtonInner = observer(
Designer: React.ElementType; Designer: React.ElementType;
designerProps: any; designerProps: any;
title: string; title: string;
isLink?: boolean;
}) => { }) => {
const { const {
designable, designable,
@ -558,6 +558,7 @@ const RenderButtonInner = observer(
Designer, Designer,
designerProps, designerProps,
title, title,
isLink,
...others ...others
} = props; } = props;
const debouncedClick = useCallback( const debouncedClick = useCallback(
@ -582,7 +583,8 @@ const RenderButtonInner = observer(
} }
const actionTitle = title || field?.title; const actionTitle = title || field?.title;
const { opacity, ...restButtonStyle } = buttonStyle;
const linkStyle = isLink && opacity ? { opacity } : undefined;
return ( return (
<SortableItem <SortableItem
role="button" role="button"
@ -591,15 +593,19 @@ const RenderButtonInner = observer(
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
// @ts-ignore // @ts-ignore
loading={field?.data?.loading || loading} loading={field?.data?.loading || loading}
icon={typeof icon === 'string' ? <Icon type={icon} /> : icon} icon={typeof icon === 'string' ? <Icon type={icon} style={linkStyle} /> : icon}
disabled={disabled} disabled={disabled}
style={buttonStyle} style={isLink ? restButtonStyle : buttonStyle}
onClick={process.env.__E2E__ ? handleButtonClick : debouncedClick} // E2E 中的点击操作都是很快的,如果加上 debounce 会导致 E2E 测试失败 onClick={process.env.__E2E__ ? handleButtonClick : debouncedClick} // E2E 中的点击操作都是很快的,如果加上 debounce 会导致 E2E 测试失败
component={tarComponent || Button} component={tarComponent || Button}
className={classnames(componentCls, hashId, className, 'nb-action')} className={classnames(componentCls, hashId, className, 'nb-action')}
type={type === 'danger' ? undefined : type} type={type === 'danger' ? undefined : type}
> >
{actionTitle && <span className={icon ? 'nb-action-title' : null}>{actionTitle}</span>} {actionTitle && (
<span className={icon ? 'nb-action-title' : null} style={linkStyle}>
{actionTitle}
</span>
)}
<Designer {...designerProps} /> <Designer {...designerProps} />
</SortableItem> </SortableItem>
); );

View File

@ -14,6 +14,7 @@ import React, { useEffect, useMemo, useState } from 'react';
import { useAPIClient, useRequest } from '../../../api-client'; import { useAPIClient, useRequest } from '../../../api-client';
import { useCollectionManager } from '../../../data-source/collection'; import { useCollectionManager } from '../../../data-source/collection';
import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord'; import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord';
import { getDataSourceHeaders } from '../../../data-source/utils';
import { useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive'; import { useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
import { useSchemaComponentContext } from '../../hooks'; import { useSchemaComponentContext } from '../../hooks';
import { AssociationFieldContext } from './context'; import { AssociationFieldContext } from './context';
@ -67,9 +68,11 @@ export const AssociationFieldProvider = observer(
if (_.isUndefined(ids) || _.isNil(ids) || _.isNaN(ids)) { if (_.isUndefined(ids) || _.isNil(ids) || _.isNaN(ids)) {
return Promise.reject(null); return Promise.reject(null);
} }
return api.request({ return api.request({
resource: collectionField.target, resource: collectionField.target,
action: Array.isArray(ids) ? 'list' : 'get', action: Array.isArray(ids) ? 'list' : 'get',
headers: getDataSourceHeaders(cm?.dataSource?.key),
params: { params: {
filter: { filter: {
[targetKey]: ids, [targetKey]: ids,

View File

@ -14,22 +14,28 @@ import { uid } from '@formily/shared';
import { Space, message } from 'antd'; import { Space, message } from 'antd';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { isFunction } from 'mathjs'; import { isFunction } from 'mathjs';
import React, { useEffect, useState, useContext } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
ClearCollectionFieldContext, ClearCollectionFieldContext,
NocoBaseRecursionField, NocoBaseRecursionField,
RecordProvider, RecordProvider,
SchemaComponentContext,
useAPIClient, useAPIClient,
useCollectionRecordData, useCollectionRecordData,
SchemaComponentContext, useCollectionManager_deprecated,
} from '../../../'; } from '../../../';
import { Action } from '../action'; import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
import { isVariable } from '../../../variables/utils/isVariable'; import { isVariable } from '../../../variables/utils/isVariable';
import { getInnermostKeyAndValue } from '../../common/utils/uitls'; import { getInnermostKeyAndValue } from '../../common/utils/uitls';
import { Action } from '../action';
import { RemoteSelect, RemoteSelectProps } from '../remote-select'; import { RemoteSelect, RemoteSelectProps } from '../remote-select';
import useServiceOptions, { useAssociationFieldContext } from './hooks'; import useServiceOptions, { useAssociationFieldContext } from './hooks';
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
const removeIfKeyEmpty = (obj, filterTargetKey) => {
if (!obj || typeof obj !== 'object' || !filterTargetKey || Array.isArray(obj)) return obj;
return !obj[filterTargetKey] ? null : obj;
};
export const AssociationFieldAddNewer = (props) => { export const AssociationFieldAddNewer = (props) => {
const schemaComponentCtxValue = useContext(SchemaComponentContext); const schemaComponentCtxValue = useContext(SchemaComponentContext);
@ -69,6 +75,11 @@ export const filterAnalyses = (filters): any[] => {
return results; return results;
}; };
function getFieldPath(str) {
const lastIndex = str.lastIndexOf('.');
return lastIndex === -1 ? str : str.slice(0, lastIndex);
}
const InternalAssociationSelect = observer( const InternalAssociationSelect = observer(
(props: AssociationSelectProps) => { (props: AssociationSelectProps) => {
const { objectValue = true, addMode: propsAddMode, ...rest } = props; const { objectValue = true, addMode: propsAddMode, ...rest } = props;
@ -88,6 +99,9 @@ const InternalAssociationSelect = observer(
const resource = api.resource(collectionField.target); const resource = api.resource(collectionField.target);
const recordData = useCollectionRecordData(); const recordData = useCollectionRecordData();
const schemaComponentCtxValue = useContext(SchemaComponentContext); const schemaComponentCtxValue = useContext(SchemaComponentContext);
const { getCollection } = useCollectionManager_deprecated();
const associationCollection = getCollection(collectionField.target);
const { filterTargetKey } = associationCollection;
useEffect(() => { useEffect(() => {
const initValue = isVariable(field.value) ? undefined : field.value; const initValue = isVariable(field.value) ? undefined : field.value;
@ -100,11 +114,14 @@ const InternalAssociationSelect = observer(
//支持深层次子表单 //支持深层次子表单
onFieldInputValueChange('*', (fieldPath: any) => { onFieldInputValueChange('*', (fieldPath: any) => {
const linkageFields = filterAnalyses(field.componentProps?.service?.params?.filter) || []; const linkageFields = filterAnalyses(field.componentProps?.service?.params?.filter) || [];
const linageFieldEntire = getFieldPath(fieldPath.address.entire);
const targetFieldEntire = getFieldPath(field.address.entire);
if ( if (
linkageFields.includes(fieldPath?.props?.name) && linkageFields.includes(fieldPath?.props?.name) &&
field.value && field.value &&
isEqual(fieldPath?.indexes, field?.indexes) && isEqual(fieldPath?.indexes, field?.indexes) &&
fieldPath?.props?.name !== field.props.name fieldPath?.props?.name !== field.props.name &&
(!field?.indexes?.length || isEqual(linageFieldEntire, targetFieldEntire))
) { ) {
field.setValue(null); field.setValue(null);
setInnerValue(null); setInnerValue(null);
@ -151,7 +168,6 @@ const InternalAssociationSelect = observer(
</div> </div>
); );
}; };
console.log(fieldSchema);
return ( return (
<div key={fieldSchema.name}> <div key={fieldSchema.name}>
<Space.Compact style={{ display: 'flex' }}> <Space.Compact style={{ display: 'flex' }}>
@ -160,7 +176,7 @@ const InternalAssociationSelect = observer(
{...rest} {...rest}
size={'middle'} size={'middle'}
objectValue={objectValue} objectValue={objectValue}
value={value || innerValue} value={removeIfKeyEmpty(value || innerValue, filterTargetKey)}
service={service} service={service}
onChange={(value) => { onChange={(value) => {
const val = value?.length !== 0 ? value : null; const val = value?.length !== 0 ? value : null;

View File

@ -13,6 +13,8 @@ import { FormProvider, connect, createSchemaField, observer, useField, useFieldS
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Select as AntdSelect, Input, Space, Spin, Tag } from 'antd'; import { Select as AntdSelect, Input, Space, Spin, Tag } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { css } from '@emotion/css';
import { debounce } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient, useCollectionManager_deprecated } from '../../../'; import { useAPIClient, useCollectionManager_deprecated } from '../../../';
@ -152,7 +154,11 @@ const CascadeSelect = connect((props) => {
} else { } else {
associationField.value = option; associationField.value = option;
} }
onChange?.(options); if (options.length === 1 && !options[0].value) {
onChange?.(null);
} else {
onChange?.(options);
}
}; };
const onDropdownVisibleChange = async (visible, selectedValue, index) => { const onDropdownVisibleChange = async (visible, selectedValue, index) => {
@ -238,28 +244,38 @@ export const InternalCascadeSelect = observer(
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { loading, data: formData } = useDataBlockRequest() || {}; const { loading, data: formData } = useDataBlockRequest() || {};
const initialValue = formData?.data?.[fieldSchema.name]; const initialValue = formData?.data?.[fieldSchema.name];
const handleFormValuesChange = debounce((form) => {
if (collectionField.interface === 'm2o') {
// 对 m2o 类型字段,提取最后一个非 null 值
const value = extractLastNonNullValueObjects(form.values?.[fieldSchema.name]);
setTimeout(() => {
form.setValuesIn(fieldSchema.name, value);
field.value = value;
});
} else {
// 对 select_array 类型字段,过滤掉空对象
const value = extractLastNonNullValueObjects(form.values?.select_array).filter(
(v) => v && Object.keys(v).length > 0,
);
setTimeout(() => {
field.value = value;
});
}
}, 300);
useEffect(() => { useEffect(() => {
const id = uid(); const id = uid();
selectForm.addEffects(id, () => { selectForm.addEffects(id, () => {
onFormValuesChange((form) => { onFormValuesChange((form) => {
if (collectionField.interface === 'm2o') { handleFormValuesChange(form);
const value = extractLastNonNullValueObjects(form.values?.[fieldSchema.name]);
setTimeout(() => {
form.setValuesIn(fieldSchema.name, value);
field.value = value;
});
} else {
const value = extractLastNonNullValueObjects(form.values?.select_array).filter(
(v) => v && Object.keys(v).length > 0,
);
setTimeout(() => {
field.value = value;
});
}
}); });
}); });
return () => { return () => {
selectForm.removeEffects(id); selectForm.removeEffects(id);
// 清除防抖定时器
handleFormValuesChange.cancel();
}; };
}, []); }, []);
@ -282,6 +298,24 @@ export const InternalCascadeSelect = observer(
items: { items: {
type: 'void', type: 'void',
'x-component': 'Space', 'x-component': 'Space',
'x-component-props': {
style: {
width: '100%',
display: 'flex',
},
className: css`
.ant-formily-item-control {
max-width: 100% !important;
}
.ant-space-item:nth-child(1) {
flex: 0.1;
}
.ant-space-item:nth-child(2) {
flex: 3;
}
`,
},
properties: { properties: {
sort: { sort: {
type: 'void', type: 'void',

View File

@ -256,8 +256,7 @@ export const SubTable: any = observer(
{field.editable && ( {field.editable && (
<Space <Space
style={{ style={{
marginTop: '10px', position: 'relative',
position: field.value?.length ? 'absolute' : 'relative',
bottom: '0', bottom: '0',
gap: 15, gap: 15,
}} }}

View File

@ -602,12 +602,6 @@ const InternalNocoBaseTable = React.memo(
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.ant-table-expanded-row-fixed {
min-height: ${tableHeight}px;
}
.ant-table-body {
min-height: ${tableHeight}px;
}
.ant-table-cell { .ant-table-cell {
padding: 16px 8px; padding: 16px 8px;
} }

View File

@ -53,6 +53,7 @@ export function useAssociationFieldContext<F extends GeneralField>() {
}; };
} }
// 用于获取关系字段请求数据时所需的一些参数
export default function useServiceOptions(props) { export default function useServiceOptions(props) {
const { action = 'list', service, useOriginalFilter } = props; const { action = 'list', service, useOriginalFilter } = props;
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();

View File

@ -54,7 +54,7 @@ describe('CollectionSelect', () => {
role="button" role="button"
> >
<div <div
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-11aiz3o" class="css-a7w9kk ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-11aiz3o"
> >
<div <div
class="ant-formily-item-label" class="ant-formily-item-label"
@ -191,7 +191,7 @@ describe('CollectionSelect', () => {
role="button" role="button"
> >
<div <div
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-11aiz3o" class="css-a7w9kk ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-11aiz3o"
> >
<div <div
class="ant-formily-item-label" class="ant-formily-item-label"

View File

@ -289,7 +289,7 @@ DatePicker.FilterWithPicker = function FilterWithPicker(props: any) {
const [stateProps, setStateProps] = useState(newProps); const [stateProps, setStateProps] = useState(newProps);
useEffect(() => { useEffect(() => {
newProps.picker = targetPicker; newProps.picker = targetPicker;
const dateTimeFormat = getDateTimeFormat(targetPicker, format, showTime, timeFormat); const dateTimeFormat = getDateTimeFormat(targetPicker, targetDateFormat, showTime, timeFormat);
newProps.format = dateTimeFormat; newProps.format = dateTimeFormat;
setStateProps(newProps); setStateProps(newProps);
}, [targetPicker]); }, [targetPicker]);

View File

@ -65,7 +65,6 @@ export const DynamicComponent = (props: Props) => {
...props.style, ...props.style,
}, },
utc: false, utc: false,
underFilter: true,
}), }),
name: 'value', name: 'value',
'x-read-pretty': false, 'x-read-pretty': false,

View File

@ -70,6 +70,7 @@ export const FilterItem = observer(
className={css` className={css`
width: 160px; width: 160px;
`} `}
showSearch
fieldNames={fieldNames} fieldNames={fieldNames}
changeOnSelect={false} changeOnSelect={false}
value={dataIndex} value={dataIndex}

View File

@ -92,7 +92,7 @@ export const useGetFilterFieldOptions = () => {
const getOptions = (fields, depth, usedInVariable?: boolean) => { const getOptions = (fields, depth, usedInVariable?: boolean) => {
const options = []; const options = [];
fields.forEach((field) => { fields?.forEach((field) => {
const option = field2option(field, depth, usedInVariable); const option = field2option(field, depth, usedInVariable);
if (option) { if (option) {
options.push(option); options.push(option);

View File

@ -985,12 +985,11 @@ function useFormItemCollectionField() {
export function useIsAssociationField() { export function useIsAssociationField() {
const collectionField = useFormItemCollectionField(); const collectionField = useFormItemCollectionField();
const isAssociationField = ['obo', 'oho', 'o2o', 'o2m', 'm2m', 'm2o', 'updatedBy', 'createdBy', 'mbm'].includes( const isAssociationField =
collectionField?.interface, collectionField &&
); ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(collectionField.type);
return isAssociationField; return isAssociationField;
} }
export function useIsFileField() { export function useIsFileField() {
const cm = useCollectionManager(); const cm = useCollectionManager();
const collectionField = useFormItemCollectionField(); const collectionField = useFormItemCollectionField();

View File

@ -37,6 +37,17 @@ const formItemWrapCss = css`
.ant-description-textarea img { .ant-description-textarea img {
max-width: 100%; max-width: 100%;
} }
&.ant-formily-item-layout-horizontal.ant-formily-item-label-wrap {
.ant-formily-item-label {
display: inline;
padding-right: 5px;
.ant-formily-item-label-tooltip-icon,
.ant-formily-item-label-content {
display: inline;
}
}
}
`; `;
const formItemLabelCss = css` const formItemLabelCss = css`
@ -44,7 +55,7 @@ const formItemLabelCss = css`
padding: 0px !important; padding: 0px !important;
} }
> .ant-formily-item-label { > .ant-formily-item-label {
display: none; display: none !important;
} }
`; `;
@ -83,7 +94,6 @@ export const FormItem: any = withDynamicSchemaProps(
[formItemLabelCss]: showTitle === false, [formItemLabelCss]: showTitle === false,
}); });
}, [showTitle]); }, [showTitle]);
// 联动规则中的“隐藏保留值”的效果 // 联动规则中的“隐藏保留值”的效果
if (field.data?.hidden) { if (field.data?.hidden) {
return null; return null;

View File

@ -86,11 +86,6 @@ const useParseDefaultValue = () => {
field && field &&
((isVariable(fieldSchema.default) && field.value == null) || field.value === fieldSchema.default || forceUpdate) ((isVariable(fieldSchema.default) && field.value == null) || field.value === fieldSchema.default || forceUpdate)
) { ) {
// 一个变量字符串如果显示出来会比较奇怪
if (isVariable(field.value)) {
await field.reset({ forceClear: true });
}
field.loading = true; field.loading = true;
const collectionField = !fieldSchema.name.toString().includes('.') && collection?.getField(fieldSchema.name); const collectionField = !fieldSchema.name.toString().includes('.') && collection?.getField(fieldSchema.name);

View File

@ -16,8 +16,10 @@ import { ConfigProvider, theme } from 'antd';
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import { useActionContext } from '..'; import { useActionContext } from '..';
import { useAttach, useComponent } from '../..'; import { useAttach, useComponent } from '../..';
import { useApp } from '../../../application';
import { getCardItemSchema } from '../../../block-provider'; import { getCardItemSchema } from '../../../block-provider';
import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider'; import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider';
import { useDataBlockProps } from '../../../data-source';
import { useDataBlockRequest } from '../../../data-source/data-block/DataBlockRequestProvider'; import { useDataBlockRequest } from '../../../data-source/data-block/DataBlockRequestProvider';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField'; import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
@ -27,7 +29,6 @@ import { useToken } from '../../../style';
import { useLocalVariables, useVariables } from '../../../variables'; import { useLocalVariables, useVariables } from '../../../variables';
import { useProps } from '../../hooks/useProps'; import { useProps } from '../../hooks/useProps';
import { useFormBlockHeight } from './hook'; import { useFormBlockHeight } from './hook';
import { useApp } from '../../../application';
export interface FormProps extends IFormLayoutProps { export interface FormProps extends IFormLayoutProps {
form?: FormilyForm; form?: FormilyForm;
@ -141,12 +142,15 @@ const WithForm = (props: WithFormProps) => {
const linkageRules: any[] = const linkageRules: any[] =
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || []; (getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
// 关闭弹窗之前,如果有未保存的数据,是否要二次确认
const { confirmBeforeClose = true } = useDataBlockProps() || ({} as any);
useEffect(() => { useEffect(() => {
const id = uid(); const id = uid();
form.addEffects(id, () => { form.addEffects(id, () => {
onFormInputChange(() => { onFormInputChange(() => {
setFormValueChanged?.(true); setFormValueChanged?.(confirmBeforeClose);
}); });
}); });
@ -157,7 +161,7 @@ const WithForm = (props: WithFormProps) => {
return () => { return () => {
form.removeEffects(id); form.removeEffects(id);
}; };
}, [form, props.disabled, setFormValueChanged]); }, [form, props.disabled, setFormValueChanged, confirmBeforeClose]);
useEffect(() => { useEffect(() => {
if (loading) { if (loading) {
@ -210,17 +214,19 @@ const WithForm = (props: WithFormProps) => {
const WithoutForm = (props) => { const WithoutForm = (props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { setFormValueChanged } = useActionContext(); const { setFormValueChanged } = useActionContext();
// 关闭弹窗之前,如果有未保存的数据,是否要二次确认
const { confirmBeforeClose = true } = useDataBlockProps() || ({} as any);
const form = useMemo( const form = useMemo(
() => () =>
createForm({ createForm({
disabled: props.disabled, disabled: props.disabled,
effects() { effects() {
onFormInputChange((form) => { onFormInputChange((form) => {
setFormValueChanged?.(true); setFormValueChanged?.(confirmBeforeClose);
}); });
}, },
}), }),
[], [confirmBeforeClose],
); );
return fieldSchema['x-decorator'] === 'FormV2' ? ( return fieldSchema['x-decorator'] === 'FormV2' ? (
<FormDecorator form={form} {...props} /> <FormDecorator form={form} {...props} />

View File

@ -20,7 +20,6 @@ import { FilterBlockProvider } from '../../../filter-provider/FilterProvider';
import { import {
NocoBaseRecursionField, NocoBaseRecursionField,
RefreshComponentProvider, RefreshComponentProvider,
useRefreshComponent,
useRefreshFieldSchema, useRefreshFieldSchema,
} from '../../../formily/NocoBaseRecursionField'; } from '../../../formily/NocoBaseRecursionField';
import { DndContext, DndContextProps } from '../../common/dnd-context'; import { DndContext, DndContextProps } from '../../common/dnd-context';
@ -379,11 +378,9 @@ export const Grid: any = observer(
}, [fieldSchema, render, InitializerComponent, showDivider]); }, [fieldSchema, render, InitializerComponent, showDivider]);
const refreshFieldSchema = useRefreshFieldSchema(); const refreshFieldSchema = useRefreshFieldSchema();
const refreshComponent = useRefreshComponent();
const refresh = useCallback(() => { const refresh = useCallback(() => {
refreshFieldSchema?.(); refreshFieldSchema?.();
refreshComponent?.(); }, [refreshFieldSchema]);
}, [refreshComponent, refreshFieldSchema]);
return ( return (
<RefreshComponentProvider refresh={refresh}> <RefreshComponentProvider refresh={refresh}>

View File

@ -11,22 +11,40 @@ import { LoadingOutlined } from '@ant-design/icons';
import { connect, mapProps, mapReadPretty } from '@formily/react'; import { connect, mapProps, mapReadPretty } from '@formily/react';
import { Input as AntdInput } from 'antd'; import { Input as AntdInput } from 'antd';
import { InputProps, TextAreaProps } from 'antd/es/input'; import { InputProps, TextAreaProps } from 'antd/es/input';
import React from 'react'; import React, { useCallback } from 'react';
import { JSONTextAreaProps, Json } from './Json'; import { JSONTextAreaProps, Json } from './Json';
import { InputReadPrettyComposed, ReadPretty } from './ReadPretty'; import { InputReadPrettyComposed, ReadPretty } from './ReadPretty';
export { ReadPretty as InputReadPretty } from './ReadPretty'; export { ReadPretty as InputReadPretty } from './ReadPretty';
type ComposedInput = React.FC<InputProps> & { type ComposedInput = React.FC<NocoBaseInputProps> & {
ReadPretty: InputReadPrettyComposed['Input']; ReadPretty: InputReadPrettyComposed['Input'];
TextArea: React.FC<TextAreaProps> & { ReadPretty: InputReadPrettyComposed['TextArea'] }; TextArea: React.FC<TextAreaProps> & { ReadPretty: InputReadPrettyComposed['TextArea'] };
URL: React.FC<InputProps> & { ReadPretty: InputReadPrettyComposed['URL'] }; URL: React.FC<InputProps> & { ReadPretty: InputReadPrettyComposed['URL'] };
JSON: React.FC<JSONTextAreaProps> & { ReadPretty: InputReadPrettyComposed['JSON'] }; JSON: React.FC<JSONTextAreaProps> & { ReadPretty: InputReadPrettyComposed['JSON'] };
}; };
export type NocoBaseInputProps = InputProps & {
trim?: boolean;
};
function InputInner(props: NocoBaseInputProps) {
const { onChange, trim, ...others } = props;
const handleChange = useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
if (trim) {
ev.target.value = ev.target.value.trim();
}
onChange?.(ev);
},
[onChange, trim],
);
return <AntdInput {...others} onChange={handleChange} />;
}
export const Input: ComposedInput = Object.assign( export const Input: ComposedInput = Object.assign(
connect( connect(
AntdInput, InputInner,
mapProps((props, field) => { mapProps((props, field) => {
return { return {
...props, ...props,

View File

@ -1,4 +1,3 @@
import React from 'react'; import React from 'react';
import { mockApp } from '@nocobase/client/demo-utils'; import { mockApp } from '@nocobase/client/demo-utils';
import { SchemaComponent, Plugin, ISchema } from '@nocobase/client'; import { SchemaComponent, Plugin, ISchema } from '@nocobase/client';
@ -15,15 +14,25 @@ const schema: ISchema = {
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input', 'x-component': 'Input',
}, },
trim: {
type: 'string',
title: `Trim heading and tailing spaces`,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
trim: true,
},
},
}, },
} };
const Demo = () => { const Demo = () => {
return <SchemaComponent schema={schema} />; return <SchemaComponent schema={schema} />;
}; };
class DemoPlugin extends Plugin { class DemoPlugin extends Plugin {
async load() { async load() {
this.app.router.add('root', { path: '/', Component: Demo }) this.app.router.add('root', { path: '/', Component: Demo });
} }
} }

View File

@ -273,31 +273,43 @@ const InternalPagePopups = (props: { paramsList?: PopupParams[] }) => {
); );
}); });
const schemas = await Promise.all(waitList); const schemas = await Promise.all(waitList);
const clonedSchemas = schemas.map((schema, index) => { const clonedSchemas = await Promise.all(
if (_.isEmpty(schema)) { schemas.map(async (schema, index) => {
return get404Schema(); if (_.isEmpty(schema)) {
} return get404Schema();
const params = popupParams[index];
if (params.puid) {
const popupSchema = findSchemaByUid(params.puid, fieldSchema?.root);
if (popupSchema) {
savePopupSchemaToSchema(_.omit(popupSchema, 'parent'), schema);
} }
}
// Using toJSON for deep clone, faster than lodash's cloneDeep const params = popupParams[index];
const result = _.cloneDeepWith(_.omit(schema, 'parent'), (value) => {
// If we clone the Tabs component, it will cause the configuration to be lost when reopening the popup after modifying its settings if (params.puid) {
if (value?.['x-component'] === 'Tabs') { const popupSchema = findSchemaByUid(params.puid, fieldSchema?.root);
return value; if (popupSchema) {
savePopupSchemaToSchema(_.omit(popupSchema, 'parent'), schema);
} else {
// 当本地找不到 popupSchema 时,通过接口请求 puid 对应的 schema
try {
const remoteSchema = await requestSchema(params.puid);
if (remoteSchema) {
savePopupSchemaToSchema(remoteSchema, schema);
}
} catch (error) {
console.error('Failed to fetch schema for puid:', params.puid, error);
}
}
} }
});
result['x-read-pretty'] = true;
return result; // Using toJSON for deep clone, faster than lodash's cloneDeep
}); const result = _.cloneDeepWith(_.omit(schema, 'parent'), (value) => {
// If we clone the Tabs component, it will cause the configuration to be lost when reopening the popup after modifying its settings
if (value?.['x-component'] === 'Tabs') {
return value;
}
});
result['x-read-pretty'] = true;
return result;
}),
);
popupPropsRef.current = clonedSchemas.map((schema, index, items) => { popupPropsRef.current = clonedSchemas.map((schema, index, items) => {
const schemaContext = getPopupContextFromActionOrAssociationFieldSchema(schema); const schemaContext = getPopupContextFromActionOrAssociationFieldSchema(schema);
let hidden = false; let hidden = false;

View File

@ -1,5 +0,0 @@
# Page
Can be used in conjunction with DocumentTitleProvider to display the page title on document.title.
<code src="./demos/demo1.tsx"></code>

View File

@ -1,5 +0,0 @@
# Page
可以与 DocumentTitleProvider 搭配使用,将 page title 显示在 document.title 上。
<code src="./demos/demo1.tsx"></code>

View File

@ -27,10 +27,10 @@ export const mapTimeFormat = function () {
...props, ...props,
format, format,
inputReadOnly: true, inputReadOnly: true,
value: dayjsable(props.value, format), value: dayjsable(props.value, 'HH:mm:ss'),
onChange: (value: dayjs.Dayjs | dayjs.Dayjs[]) => { onChange: (value: dayjs.Dayjs | dayjs.Dayjs[]) => {
if (onChange) { if (onChange) {
onChange(formatDayjsValue(value, format) || null); onChange(formatDayjsValue(value, 'HH:mm:ss') || null);
} }
}, },
}; };

View File

@ -11,6 +11,7 @@ import { DeleteOutlined, DownloadOutlined, InboxOutlined, LoadingOutlined, PlusO
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { connect, mapProps, mapReadPretty, useField } from '@formily/react'; import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
import { Alert, Upload as AntdUpload, Button, Modal, Progress, Space, Tooltip } from 'antd'; import { Alert, Upload as AntdUpload, Button, Modal, Progress, Space, Tooltip } from 'antd';
import { createGlobalStyle } from 'antd-style';
import useUploadStyle from 'antd/es/upload/style'; import useUploadStyle from 'antd/es/upload/style';
import cls from 'classnames'; import cls from 'classnames';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
@ -36,6 +37,12 @@ import {
import { useStyles } from './style'; import { useStyles } from './style';
import type { ComposedUpload, DraggerProps, DraggerV2Props, UploadProps } from './type'; import type { ComposedUpload, DraggerProps, DraggerV2Props, UploadProps } from './type';
const LightBoxGlobalStyle = createGlobalStyle`
.ReactModal__Overlay.ReactModal__Overlay--after-open {
z-index: 3000 !important; // 避免预览图片时被遮挡
}
`;
attachmentFileTypes.add({ attachmentFileTypes.add({
match(file) { match(file) {
return matchMimetype(file, 'image/*'); return matchMimetype(file, 'image/*');
@ -62,34 +69,37 @@ attachmentFileTypes.add({
[index, list], [index, list],
); );
return ( return (
<LightBox <>
// discourageDownloads={true} <LightBoxGlobalStyle />
mainSrc={list[index]?.url} <LightBox
nextSrc={list[(index + 1) % list.length]?.url} // discourageDownloads={true}
prevSrc={list[(index + list.length - 1) % list.length]?.url} mainSrc={list[index]?.url}
onCloseRequest={() => onSwitchIndex(null)} nextSrc={list[(index + 1) % list.length]?.url}
onMovePrevRequest={() => onSwitchIndex((index + list.length - 1) % list.length)} prevSrc={list[(index + list.length - 1) % list.length]?.url}
onMoveNextRequest={() => onSwitchIndex((index + 1) % list.length)} onCloseRequest={() => onSwitchIndex(null)}
imageTitle={list[index]?.title} onMovePrevRequest={() => onSwitchIndex((index + list.length - 1) % list.length)}
toolbarButtons={[ onMoveNextRequest={() => onSwitchIndex((index + 1) % list.length)}
<button imageTitle={list[index]?.title}
key={'preview-img'} toolbarButtons={[
style={{ fontSize: 22, background: 'none', lineHeight: 1 }} <button
type="button" key={'preview-img'}
aria-label="Download" style={{ fontSize: 22, background: 'none', lineHeight: 1 }}
title="Download" type="button"
className="ril-zoom-in ril__toolbarItemChild ril__builtinButton" aria-label="Download"
onClick={onDownload} title="Download"
> className="ril-zoom-in ril__toolbarItemChild ril__builtinButton"
<DownloadOutlined /> onClick={onDownload}
</button>, >
]} <DownloadOutlined />
/> </button>,
]}
/>
</>
); );
}, },
}); });
const iframePreviewSupportedTypes = ['application/pdf', 'audio/*', 'image/*', 'video/*', 'text/*']; const iframePreviewSupportedTypes = ['application/pdf', 'audio/*', 'image/*', 'video/*', 'text/plain'];
function IframePreviewer({ index, list, onSwitchIndex }) { function IframePreviewer({ index, list, onSwitchIndex }) {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -26,12 +26,13 @@ import { Json } from '../input';
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/; const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
type ParseOptions = { type ParseOptions = {
defaultTypeOnNull?: string;
stringToDate?: boolean; stringToDate?: boolean;
}; };
function parseValue(value: any, options: ParseOptions = {}): string | string[] { function parseValue(value: any, options: ParseOptions = {}): string | string[] {
if (value == null) { if (value == null) {
return 'null'; return options.defaultTypeOnNull ?? 'null';
} }
const type = typeof value; const type = typeof value;
if (type === 'string') { if (type === 'string') {

View File

@ -7,9 +7,9 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import React, { useRef, useState } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Button, Input } from 'antd'; import { Button, Input } from 'antd';
import React, { useRef, useState } from 'react';
import { VariableSelect } from './VariableSelect'; import { VariableSelect } from './VariableSelect';
// NOTE: https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-onchange-event-in-react-js/46012210#46012210 // NOTE: https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-onchange-event-in-react-js/46012210#46012210
@ -80,6 +80,7 @@ export function RawTextArea(props): JSX.Element {
setOptions={setOptions} setOptions={setOptions}
onInsert={onInsert} onInsert={onInsert}
changeOnSelect={changeOnSelect} changeOnSelect={changeOnSelect}
disabled={others.disabled}
/> />
</Button.Group> </Button.Group>
</div> </div>

View File

@ -10,6 +10,7 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { useForm } from '@formily/react'; import { useForm } from '@formily/react';
import { Space, theme } from 'antd'; import { Space, theme } from 'antd';
import type { CascaderProps, DefaultOptionType } from 'antd/lib/cascader';
import useInputStyle from 'antd/es/input/style'; import useInputStyle from 'antd/es/input/style';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { renderToString } from 'react-dom/server'; import { renderToString } from 'react-dom/server';
@ -110,7 +111,7 @@ function renderHTML(exp: string, keyLabelMap, delimiters: [string, string] = ['{
}); });
} }
function createOptionsValueLabelMap(options: any[], fieldNames = { value: 'value', label: 'label' }) { function createOptionsValueLabelMap(options: any[], fieldNames: CascaderProps['fieldNames'] = defaultFieldNames) {
const map = new Map<string, string[]>(); const map = new Map<string, string[]>();
for (const option of options) { for (const option of options) {
map.set(option[fieldNames.value], [option[fieldNames.label]]); map.set(option[fieldNames.value], [option[fieldNames.label]]);
@ -220,10 +221,24 @@ function useVariablesFromValue(value: string, delimiters: [string, string] = ['{
}, [value, delimitersString]); }, [value, delimitersString]);
} }
export function TextArea(props) { export type TextAreaProps = {
value?: string;
scope?: Partial<DefaultOptionType>[] | (() => Partial<DefaultOptionType>[]);
onChange?(value: string): void;
disabled?: boolean;
changeOnSelect?: CascaderProps['changeOnSelect'];
style?: React.CSSProperties;
fieldNames?: CascaderProps['fieldNames'];
trim?: boolean;
delimiters?: [string, string];
addonBefore?: React.ReactNode;
};
export function TextArea(props: TextAreaProps) {
const { wrapSSR, hashId, componentCls } = useStyles(); const { wrapSSR, hashId, componentCls } = useStyles();
const { scope, onChange, changeOnSelect, style, fieldNames, delimiters = ['{{', '}}'], addonBefore } = props; const { scope, changeOnSelect, style, fieldNames, delimiters = ['{{', '}}'], addonBefore, trim = true } = props;
const value = typeof props.value === 'string' ? props.value : props.value == null ? '' : props.value.toString(); const value =
typeof props.value === 'string' ? props.value : props.value == null ? '' : (props.value as any).toString();
const variables = useVariablesFromValue(value, delimiters); const variables = useVariablesFromValue(value, delimiters);
const inputRef = useRef<HTMLDivElement>(null); const inputRef = useRef<HTMLDivElement>(null);
const [options, setOptions] = useState([]); const [options, setOptions] = useState([]);
@ -241,6 +256,14 @@ export function TextArea(props) {
const { token } = theme.useToken(); const { token } = theme.useToken();
const delimitersString = delimiters.join(' '); const delimitersString = delimiters.join(' ');
const onChange = useCallback(
(target: HTMLDivElement) => {
const v = getValue(target, delimiters);
props.onChange?.(trim ? v.trim() : v);
},
[delimitersString, props.onChange, trim],
);
useEffect(() => { useEffect(() => {
preloadOptions(scope, variables) preloadOptions(scope, variables)
.then((preloaded) => { .then((preloaded) => {
@ -324,9 +347,9 @@ export function TextArea(props) {
setChanged(true); setChanged(true);
setRange(getCurrentRange(current)); setRange(getCurrentRange(current));
onChange(getValue(current, delimiters)); onChange(current);
}, },
[keyLabelMap, onChange, range, delimitersString], [keyLabelMap, onChange, range],
); );
const onInput = useCallback( const onInput = useCallback(
@ -336,9 +359,9 @@ export function TextArea(props) {
} }
setChanged(true); setChanged(true);
setRange(getCurrentRange(currentTarget)); setRange(getCurrentRange(currentTarget));
onChange(getValue(currentTarget, delimiters)); onChange(currentTarget);
}, },
[ime, onChange, delimitersString], [ime, onChange],
); );
const onBlur = useCallback(function ({ currentTarget }) { const onBlur = useCallback(function ({ currentTarget }) {
@ -360,9 +383,9 @@ export function TextArea(props) {
setIME(false); setIME(false);
setChanged(true); setChanged(true);
setRange(getCurrentRange(currentTarget)); setRange(getCurrentRange(currentTarget));
onChange(getValue(currentTarget, delimiters)); onChange(currentTarget);
}, },
[onChange, delimitersString], [onChange],
); );
const onPaste = useCallback( const onPaste = useCallback(
@ -393,9 +416,9 @@ export function TextArea(props) {
setChanged(true); setChanged(true);
pasteHTML(ev.currentTarget, sanitizedHTML); pasteHTML(ev.currentTarget, sanitizedHTML);
setRange(getCurrentRange(ev.currentTarget)); setRange(getCurrentRange(ev.currentTarget));
onChange(getValue(ev.currentTarget, delimiters)); onChange(ev.currentTarget);
}, },
[onChange, delimitersString], [onChange],
); );
const disabled = props.disabled || form.disabled; const disabled = props.disabled || form.disabled;
return wrapSSR( return wrapSSR(

View File

@ -96,7 +96,6 @@ export const conditionAnalyses = async (
) => { ) => {
const type = Object.keys(ruleGroup)[0] || '$and'; const type = Object.keys(ruleGroup)[0] || '$and';
const conditions = ruleGroup[type]; const conditions = ruleGroup[type];
let results = conditions.map(async (condition) => { let results = conditions.map(async (condition) => {
if ('$and' in condition || '$or' in condition) { if ('$and' in condition || '$or' in condition) {
return await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic); return await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic);
@ -147,7 +146,10 @@ export const conditionAnalyses = async (
if (type === '$and') { if (type === '$and') {
return every(results, (v) => v); return every(results, (v) => v);
} else { } else {
return some(results, (v) => v); if (results.length) {
return some(results, (v) => v);
}
return true;
} }
}; };

View File

@ -8,7 +8,7 @@
*/ */
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { Schema } from '@formily/react'; import { Schema, useFieldSchema } from '@formily/react';
import { Spin } from 'antd'; import { Spin } from 'antd';
import React, { memo, useMemo } from 'react'; import React, { memo, useMemo } from 'react';
import { useRemoteCollectionManagerLoading } from '../../collection-manager/CollectionManagerProvider'; import { useRemoteCollectionManagerLoading } from '../../collection-manager/CollectionManagerProvider';
@ -62,6 +62,7 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
}); });
const NotFoundComponent = useComponent(NotFoundPage); const NotFoundComponent = useComponent(NotFoundPage);
const collectionManagerLoading = useRemoteCollectionManagerLoading(); const collectionManagerLoading = useRemoteCollectionManagerLoading();
const parentSchema = useFieldSchema();
if (collectionManagerLoading || loading || hidden) { if (collectionManagerLoading || loading || hidden) {
return <Spin style={{ width: '100%', marginTop: 20 }} delay={LOADING_DELAY} />; return <Spin style={{ width: '100%', marginTop: 20 }} delay={LOADING_DELAY} />;
@ -73,10 +74,20 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
} }
return noForm ? ( return noForm ? (
<SchemaComponent components={components} scope={scope} schema={schemaTransform(schema || {})} /> <SchemaComponent
components={components}
scope={scope}
schema={schemaTransform(schema || {})}
parentSchema={parentSchema}
/>
) : ( ) : (
<FormProvider form={form}> <FormProvider form={form}>
<SchemaComponent components={components} scope={scope} schema={schemaTransform(schema || {})} /> <SchemaComponent
components={components}
scope={scope}
schema={schemaTransform(schema || {})}
parentSchema={parentSchema}
/>
</FormProvider> </FormProvider>
); );
}; };

View File

@ -28,6 +28,7 @@ function toSchema(schema?: any) {
properties: { properties: {
[schema.name]: schema, [schema.name]: schema,
}, },
name: `p_${schema.name}`,
}); });
} }
return new Schema(schema); return new Schema(schema);
@ -52,58 +53,65 @@ interface DistributedProps {
*/ */
export const SchemaComponentOnChangeContext = createContext<SchemaComponentOnChange>({ onChange: _.noop }); export const SchemaComponentOnChangeContext = createContext<SchemaComponentOnChange>({ onChange: _.noop });
const RecursionSchemaComponent = memo((props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => { const RecursionSchemaComponent = memo(
const { components, scope, schema: _schema, distributed, onChange: _onChange, ...others } = props; (props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps & { parentSchema?: Schema }) => {
const ctx = useContext(SchemaComponentContext); const { components, scope, schema: _schema, distributed, onChange: _onChange, parentSchema, ...others } = props;
const schema = useMemo(() => toSchema(_schema), [_schema]); const ctx = useContext(SchemaComponentContext);
const value = useMemo( const schema = useMemo(() => toSchema(_schema), [_schema]);
() => ({ const value = useMemo(
...ctx, () => ({
distributed: ctx.distributed == false ? false : distributed, ...ctx,
/** distributed: ctx.distributed == false ? false : distributed,
* @deprecated /**
*/ * @deprecated
refresh: ctx.refresh || _.noop, */
}), refresh: ctx.refresh || _.noop,
[ctx, distributed], }),
); [ctx, distributed],
);
const { onChange: onChangeFromContext } = useContext(SchemaComponentOnChangeContext); const { onChange: onChangeFromContext } = useContext(SchemaComponentOnChangeContext);
const onChangeValue = useMemo( const onChangeValue = useMemo(
() => ({ () => ({
onChange: () => { onChange: () => {
_onChange?.(schema); _onChange?.(schema);
onChangeFromContext?.(); onChangeFromContext?.();
}, },
}), }),
[_onChange, onChangeFromContext, schema], [_onChange, onChangeFromContext, schema],
); );
return ( return (
<SchemaComponentOnChangeContext.Provider value={onChangeValue}> <SchemaComponentOnChangeContext.Provider value={onChangeValue}>
<SchemaComponentContext.Provider value={value}> <SchemaComponentContext.Provider value={value}>
<SchemaComponentOptions inherit components={components} scope={scope}> <SchemaComponentOptions inherit components={components} scope={scope}>
<NocoBaseRecursionField {...others} schema={schema} isUseFormilyField /> <NocoBaseRecursionField {...others} schema={schema} isUseFormilyField parentSchema={parentSchema} />
</SchemaComponentOptions> </SchemaComponentOptions>
</SchemaComponentContext.Provider> </SchemaComponentContext.Provider>
</SchemaComponentOnChangeContext.Provider> </SchemaComponentOnChangeContext.Provider>
); );
}); },
);
RecursionSchemaComponent.displayName = 'RecursionSchemaComponent'; RecursionSchemaComponent.displayName = 'RecursionSchemaComponent';
const MemoizedSchemaComponent = memo((props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => { const MemoizedSchemaComponent = memo(
const { schema, ...others } = props; (props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps & { parentSchema?: Schema }) => {
const s = useMemoizedSchema(schema); const { schema, parentSchema, ...others } = props;
return <RecursionSchemaComponent {...others} schema={s} />; const s = useMemoizedSchema(schema);
}); return <RecursionSchemaComponent {...others} schema={s} parentSchema={parentSchema} />;
},
);
MemoizedSchemaComponent.displayName = 'MemoizedSchemaComponent'; MemoizedSchemaComponent.displayName = 'MemoizedSchemaComponent';
export const SchemaComponent = memo( export const SchemaComponent = memo(
( (
props: (ISchemaFieldProps | IRecursionFieldProps) & { memoized?: boolean } & SchemaComponentOnChange & props: (ISchemaFieldProps | IRecursionFieldProps) & {
memoized?: boolean;
parentSchema?: Schema;
} & SchemaComponentOnChange &
DistributedProps, DistributedProps,
) => { ) => {
const { memoized, ...others } = props; const { memoized, ...others } = props;

View File

@ -37,7 +37,9 @@ const getPageHeaderHeight = (disablePageHeader, enablePageTabs, hidePageTitle, t
token.paddingContentHorizontalLG token.paddingContentHorizontalLG
); );
} }
return token.controlHeight + token.marginXS + (token.paddingXXS + 2) * 2 + token.paddingContentHorizontalLG; return (
token.controlHeight + token.marginXS + (token.paddingContentVertical + 2) * 2 + token.paddingContentHorizontalLG
);
} else { } else {
if (enablePageTabs) { if (enablePageTabs) {
return ( return (
@ -140,12 +142,12 @@ export const useDataBlockHeight = (options?: UseDataBlockHeightOptions) => {
const { heightMode, height, title, titleHeight } = heightProps || {}; const { heightMode, height, title, titleHeight } = heightProps || {};
const blockHeaderHeight = title ? titleHeight : 0; const blockHeaderHeight = title ? titleHeight : 0;
if (!heightProps?.heightMode || heightMode === HeightMode.DEFAULT) { if (!heightProps?.heightMode || heightMode === HeightMode.DEFAULT) {
return; return;
} }
if (heightMode === HeightMode.FULL_HEIGHT) { if (heightMode === HeightMode.FULL_HEIGHT) {
let res = window.innerHeight - pageFullScreenHeight; let res = window.innerHeight - pageFullScreenHeight;
console.log(res);
if (options?.removeBlockHeaderHeight) { if (options?.removeBlockHeaderHeight) {
res = res - blockHeaderHeight; res = res - blockHeaderHeight;
} }

View File

@ -268,10 +268,12 @@ function FinallyButton({
const { getCollection } = useCollectionManager_deprecated(); const { getCollection } = useCollectionManager_deprecated();
const aclCtx = useACLActionParamsContext(); const aclCtx = useACLActionParamsContext();
const buttonStyle = useMemo(() => { const buttonStyle = useMemo(() => {
const shouldApplyOpacity = designable && (field?.data?.hidden || !aclCtx);
const opacityValue = componentType !== 'link' ? (shouldApplyOpacity ? 0.1 : 1) : 1;
return { return {
opacity: designable && (field?.data?.hidden || !aclCtx) && 0.1, opacity: opacityValue,
}; };
}, [designable, field?.data?.hidden]); }, [designable, field?.data?.hidden, aclCtx, componentType]);
if (inheritsCollections?.length > 0) { if (inheritsCollections?.length > 0) {
if (!linkageFromForm) { if (!linkageFromForm) {

View File

@ -470,7 +470,6 @@ export const useFilterFormItemInitializerFields = (options?: any) => {
'x-collection-field': `${name}.${field.name}`, 'x-collection-field': `${name}.${field.name}`,
'x-component-props': { 'x-component-props': {
utc: false, utc: false,
underFilter: true,
}, },
}; };
if (isAssocField(field)) { if (isAssocField(field)) {
@ -485,7 +484,7 @@ export const useFilterFormItemInitializerFields = (options?: any) => {
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-use-decorator-props': 'useFormItemProps', 'x-use-decorator-props': 'useFormItemProps',
'x-collection-field': `${name}.${field.name}`, 'x-collection-field': `${name}.${field.name}`,
'x-component-props': { ...field.uiSchema?.['x-component-props'], utc: false, underFilter: true }, 'x-component-props': { ...field.uiSchema?.['x-component-props'], utc: false },
}; };
} }
const resultItem = { const resultItem = {
@ -580,7 +579,7 @@ const associationFieldToMenu = (
interface: field.interface, interface: field.interface,
}, },
'x-component': 'CollectionField', 'x-component': 'CollectionField',
'x-component-props': { utc: false, underFilter: true }, 'x-component-props': { utc: false },
'x-read-pretty': false, 'x-read-pretty': false,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-collection-field': `${collectionName}.${schemaName}`, 'x-collection-field': `${collectionName}.${schemaName}`,
@ -697,7 +696,7 @@ export const useFilterInheritsFormItemInitializerFields = (options?) => {
'x-component': 'CollectionField', 'x-component': 'CollectionField',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}`, 'x-collection-field': `${name}.${field.name}`,
'x-component-props': { utc: false, underFilter: true }, 'x-component-props': { utc: false },
'x-read-pretty': field?.uiSchema?.['x-read-pretty'], 'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
}; };
return { return {
@ -728,7 +727,7 @@ export const useCustomFormItemInitializerFields = (options?: any) => {
const remove = useRemoveGridFormItem(); const remove = useRemoveGridFormItem();
return currentFields return currentFields
?.filter((field) => { ?.filter((field) => {
return field?.interface && field.interface !== 'snapshot' && field.type !== 'sequence'; return !field.inherit && field?.interface && field.interface !== 'snapshot' && field.type !== 'sequence';
}) })
?.map((field) => { ?.map((field) => {
const interfaceConfig = getInterface(field.interface); const interfaceConfig = getInterface(field.interface);

View File

@ -568,13 +568,14 @@ export interface SchemaSettingsSelectItemProps
extends Omit<SchemaSettingsItemProps, 'onChange' | 'onClick'>, extends Omit<SchemaSettingsItemProps, 'onChange' | 'onClick'>,
Omit<SelectWithTitleProps, 'title' | 'defaultValue'> { Omit<SelectWithTitleProps, 'title' | 'defaultValue'> {
value?: SelectWithTitleProps['defaultValue']; value?: SelectWithTitleProps['defaultValue'];
optionRender?: (option: any, info: { index: number }) => React.ReactNode;
} }
export const SchemaSettingsSelectItem: FC<SchemaSettingsSelectItemProps> = (props) => { export const SchemaSettingsSelectItem: FC<SchemaSettingsSelectItemProps> = (props) => {
const { title, options, value, onChange, ...others } = props; const { title, options, value, onChange, optionRender, ...others } = props;
return ( return (
<SchemaSettingsItem title={title} {...others}> <SchemaSettingsItem title={title} {...others}>
<SelectWithTitle {...{ title, defaultValue: value, onChange, options }} /> <SelectWithTitle {...{ title, defaultValue: value, onChange, options, optionRender }} />
</SchemaSettingsItem> </SchemaSettingsItem>
); );
}; };

View File

@ -20,6 +20,11 @@ export const useCurrentUserContext = () => {
return useContext(CurrentUserContext); return useContext(CurrentUserContext);
}; };
export const useIsLoggedIn = () => {
const ctx = useContext(CurrentUserContext);
return !!ctx?.data?.data;
};
export const useCurrentRoles = () => { export const useCurrentRoles = () => {
const { allowAnonymous } = useACLRoleContext(); const { allowAnonymous } = useACLRoleContext();
const { data } = useCurrentUserContext(); const { data } = useCurrentUserContext();
@ -39,14 +44,18 @@ export const useCurrentRoles = () => {
export const CurrentUserProvider = (props) => { export const CurrentUserProvider = (props) => {
const api = useAPIClient(); const api = useAPIClient();
const result = useRequest<any>(() => const result = useRequest<any>(
api () =>
.request({ api
url: '/auth:check', .request({
skipNotify: true, url: '/auth:check',
skipAuth: true, skipNotify: true,
}) skipAuth: true,
.then((res) => res?.data), })
.then((res) => res?.data),
{
manual: !api.auth.token,
},
); );
const { render } = useAppSpin(); const { render } = useAppSpin();

View File

@ -18,7 +18,6 @@ import { getDataSourceHeaders } from '../data-source/utils';
import { useCompile } from '../schema-component'; import { useCompile } from '../schema-component';
import useBuiltInVariables from './hooks/useBuiltinVariables'; import useBuiltInVariables from './hooks/useBuiltinVariables';
import { VariableOption, VariablesContextType } from './types'; import { VariableOption, VariablesContextType } from './types';
import { cacheLazyLoadedValues, getCachedLazyLoadedValues } from './utils/cacheLazyLoadedValues';
import { filterEmptyValues } from './utils/filterEmptyValues'; import { filterEmptyValues } from './utils/filterEmptyValues';
import { getAction } from './utils/getAction'; import { getAction } from './utils/getAction';
import { getPath } from './utils/getPath'; import { getPath } from './utils/getPath';
@ -144,14 +143,13 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
.then((data) => { .then((data) => {
clearRequested(url); clearRequested(url);
const value = data.data.data; const value = data.data.data;
cacheLazyLoadedValues(item, currentVariablePath, value);
return value; return value;
}); });
stashRequested(url, result); stashRequested(url, result);
return result; return result;
} }
} }
return getCachedLazyLoadedValues(item, currentVariablePath) || item?.[key]; return item?.[key];
}); });
current = removeThroughCollectionFields(_.flatten(await Promise.all(result)), associationField); current = removeThroughCollectionFields(_.flatten(await Promise.all(result)), associationField);
} else if ( } else if (
@ -180,17 +178,9 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
} }
const value = data.data.data; const value = data.data.data;
if (!getCachedLazyLoadedValues(current, currentVariablePath)) {
// Cache the API response data to avoid repeated requests
cacheLazyLoadedValues(current, currentVariablePath, value);
}
current = removeThroughCollectionFields(value, associationField); current = removeThroughCollectionFields(value, associationField);
} else { } else {
current = removeThroughCollectionFields( current = removeThroughCollectionFields(getValuesByPath(current, key), associationField);
getCachedLazyLoadedValues(current, currentVariablePath) || getValuesByPath(current, key),
associationField,
);
} }
if (associationField?.target) { if (associationField?.target) {
@ -359,13 +349,8 @@ export default VariablesProvider;
function shouldToRequest(value, variableCtx: Record<string, any>, variablePath: string) { function shouldToRequest(value, variableCtx: Record<string, any>, variablePath: string) {
let result = false; let result = false;
if (getCachedLazyLoadedValues(variableCtx, variablePath)) {
return false;
}
// value may be a reactive object, using untracked to avoid unexpected autorun // value may be a reactive object, using untracked to avoid unexpected autorun
untracked(() => { untracked(() => {
// fix https://nocobase.height.app/T-2502
// Compatible with `xxx to many` and `xxx to one` subform fields and subtable fields // Compatible with `xxx to many` and `xxx to one` subform fields and subtable fields
if (JSON.stringify(value) === '[{}]' || JSON.stringify(value) === '{}') { if (JSON.stringify(value) === '[{}]' || JSON.stringify(value) === '{}') {
result = true; result = true;

View File

@ -1,25 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
const cache = new Map<Record<string, any>, any>();
export const cacheLazyLoadedValues = (variableCtx: Record<string, any>, variablePath: string, value: any) => {
const cachedValue = cache.get(variableCtx);
if (cachedValue) {
cachedValue[variablePath] = value;
} else {
cache.set(variableCtx, { [variablePath]: value });
}
};
export const getCachedLazyLoadedValues = (variableCtx: Record<string, any>, variablePath: string) => {
const cachedValue = cache.get(variableCtx);
return cachedValue?.[variablePath];
};

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
export const REGEX_OF_VARIABLE = /^\s*\{\{\s*([a-zA-Z0-9_$-.]+?)\s*\}\}\s*$/g; export const REGEX_OF_VARIABLE = /^\s*\{\{\s*([\p{L}0-9_$-.]+?)\s*\}\}\s*$/u;
export const REGEX_OF_VARIABLE_IN_EXPRESSION = /\{\{\s*([a-zA-Z0-9_$-.]+?)\s*\}\}/g; export const REGEX_OF_VARIABLE_IN_EXPRESSION = /\{\{\s*([a-zA-Z0-9_$-.]+?)\s*\}\}/g;
export const isVariable = (str: unknown) => { export const isVariable = (str: unknown) => {

View File

@ -1,6 +1,6 @@
{ {
"name": "create-nocobase-app", "name": "create-nocobase-app",
"version": "1.6.6", "version": "1.6.20",
"main": "src/index.js", "main": "src/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {

View File

@ -1,16 +1,16 @@
{ {
"name": "@nocobase/data-source-manager", "name": "@nocobase/data-source-manager",
"version": "1.6.6", "version": "1.6.20",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/actions": "1.6.6", "@nocobase/actions": "1.6.20",
"@nocobase/cache": "1.6.6", "@nocobase/cache": "1.6.20",
"@nocobase/database": "1.6.6", "@nocobase/database": "1.6.20",
"@nocobase/resourcer": "1.6.6", "@nocobase/resourcer": "1.6.20",
"@nocobase/utils": "1.6.6", "@nocobase/utils": "1.6.20",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"jsonwebtoken": "^8.5.1" "jsonwebtoken": "^8.5.1"
}, },

View File

@ -7,10 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { createMockServer, mockDatabase, supertest } from '@nocobase/test';
import { SequelizeDataSource } from '../sequelize-data-source';
import { vi } from 'vitest';
import { DataSourceManager } from '@nocobase/data-source-manager'; import { DataSourceManager } from '@nocobase/data-source-manager';
import { createMockDatabase, createMockServer, mockDatabase, supertest } from '@nocobase/test';
import { vi } from 'vitest';
import { SequelizeDataSource } from '../sequelize-data-source';
describe('example', () => { describe('example', () => {
test.skip('case1', async () => { test.skip('case1', async () => {
@ -41,7 +41,7 @@ describe('example', () => {
name: 'test2', name: 'test2',
}); });
const database = mockDatabase({ const database = await createMockDatabase({
tablePrefix: 'ds1_', tablePrefix: 'ds1_',
}); });
await database.clean({ drop: true }); await database.clean({ drop: true });
@ -82,7 +82,7 @@ describe('example', () => {
name: 'update-filter', name: 'update-filter',
}); });
const database = mockDatabase({ const database = await createMockDatabase({
tablePrefix: 'ds1_', tablePrefix: 'ds1_',
}); });
@ -128,7 +128,7 @@ describe('example', () => {
name: 'update-filter', name: 'update-filter',
}); });
const database = mockDatabase({ const database = await createMockDatabase({
tablePrefix: 'ds1_', tablePrefix: 'ds1_',
}); });
@ -198,7 +198,7 @@ describe('example', () => {
// it should be called on main datasource // it should be called on main datasource
expect(hook).toBeCalledTimes(1); expect(hook).toBeCalledTimes(1);
const database = mockDatabase({ const database = await createMockDatabase({
tablePrefix: 'ds1_', tablePrefix: 'ds1_',
}); });

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { mockDatabase } from '@nocobase/test'; import { createMockDatabase, mockDatabase } from '@nocobase/test';
import Koa from 'koa'; import Koa from 'koa';
import bodyParser from 'koa-bodyparser'; import bodyParser from 'koa-bodyparser';
import supertest from 'supertest'; import supertest from 'supertest';
@ -24,7 +24,7 @@ describe('example', () => {
await next(); await next();
}); });
app.use(dsm.middleware()); app.use(dsm.middleware());
const database = mockDatabase({ const database = await createMockDatabase({
tablePrefix: 'ds1_', tablePrefix: 'ds1_',
}); });
await database.clean({ drop: true }); await database.clean({ drop: true });

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/database", "name": "@nocobase/database",
"version": "1.6.6", "version": "1.6.20",
"description": "", "description": "",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@nocobase/logger": "1.6.6", "@nocobase/logger": "1.6.20",
"@nocobase/utils": "1.6.6", "@nocobase/utils": "1.6.20",
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"cron-parser": "4.4.0", "cron-parser": "4.4.0",

View File

@ -7,13 +7,13 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { Database } from '../../database'; import { Database, createMockDatabase } from '@nocobase/database';
import { mockDatabase } from '../index';
describe('association references', () => { describe('association references', () => {
let db: Database; let db: Database;
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = await createMockDatabase();
await db.clean({ drop: true }); await db.clean({ drop: true });
}); });

View File

@ -7,13 +7,13 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { Database, mockDatabase } from '@nocobase/database'; import { createMockDatabase, Database } from '@nocobase/database';
describe('association target key', async () => { describe('association target key', async () => {
let db: Database; let db: Database;
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase(); db = await createMockDatabase();
await db.clean({ drop: true }); await db.clean({ drop: true });
}); });

View File

@ -7,14 +7,13 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { Database } from '../database'; import { createMockDatabase, Database } from '@nocobase/database';
import { mockDatabase } from './index';
describe.skipIf(process.env['DB_DIALECT'] === 'sqlite')('collection', () => { describe.skipIf(process.env['DB_DIALECT'] === 'sqlite')('collection', () => {
let db: Database; let db: Database;
beforeEach(async () => { beforeEach(async () => {
db = mockDatabase({ db = await createMockDatabase({
logging: console.log, logging: console.log,
}); });

Some files were not shown because too many files have changed in this diff Show More