mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 05:29:26 +08:00
Merge branch 'main' of github.com:nocobase/nocobase into huoshijie/fix-import-export-permission-bug
This commit is contained in:
commit
06d4b211a1
291
CHANGELOG.md
291
CHANGELOG.md
@ -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/)
|
||||
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
|
||||
|
||||
### 🎉 New Features
|
||||
|
@ -5,6 +5,297 @@
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||
并且本项目遵循 [语义化版本](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
|
||||
|
||||
### 🎉 新特性
|
||||
|
@ -1,4 +1,4 @@
|
||||
Updated Date: February 20, 2025
|
||||
Updated Date: April 1, 2025
|
||||
|
||||
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.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.
|
||||
|
||||
@ -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.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.
|
||||
|
||||
|
@ -2,14 +2,10 @@
|
||||
|
||||
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://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-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
## リリースノート
|
||||
|
||||
リリースノートは[ブログ](https://www.nocobase.com/ja/blog/timeline)で随時更新され、週ごとにまとめて公開しています。
|
||||
</p>
|
||||
|
||||
## NocoBaseはなに?
|
||||
|
||||
@ -28,6 +24,16 @@ https://docs-cn.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. データモデル駆動
|
||||
|
22
README.md
22
README.md
@ -2,19 +2,14 @@ English | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
|
||||
|
||||
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://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-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
## Release Notes
|
||||
|
||||
Our [blog](https://www.nocobase.com/en/blog/timeline) is regularly updated with release notes and provides a weekly summary.
|
||||
</p>
|
||||
|
||||
## 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!
|
||||
|
||||
Homepage:
|
||||
@ -29,6 +24,17 @@ https://docs.nocobase.com/
|
||||
Forum:
|
||||
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
|
||||
|
||||
### 1. Data model-driven
|
||||
|
@ -2,13 +2,10 @@
|
||||
|
||||
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://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-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
## 发布日志
|
||||
我们的[博客](https://www.nocobase.com/cn/blog/timeline)会及时更新发布日志,并每周进行汇总。
|
||||
</p>
|
||||
|
||||
## NocoBase 是什么
|
||||
|
||||
@ -27,6 +24,15 @@ https://docs-cn.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,5 +1,5 @@
|
||||
{
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.20",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"npmClientArgs": ["--ignore-engines"],
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@nocobase/acl",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.20",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/resourcer": "1.6.6",
|
||||
"@nocobase/utils": "1.6.6",
|
||||
"@nocobase/resourcer": "1.6.20",
|
||||
"@nocobase/utils": "1.6.20",
|
||||
"minimatch": "^5.1.1"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@nocobase/actions",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.20",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/cache": "1.6.6",
|
||||
"@nocobase/database": "1.6.6",
|
||||
"@nocobase/resourcer": "1.6.6"
|
||||
"@nocobase/cache": "1.6.20",
|
||||
"@nocobase/database": "1.6.20",
|
||||
"@nocobase/resourcer": "1.6.20"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@nocobase/app",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.20",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/database": "1.6.6",
|
||||
"@nocobase/preset-nocobase": "1.6.6",
|
||||
"@nocobase/server": "1.6.6"
|
||||
"@nocobase/database": "1.6.20",
|
||||
"@nocobase/preset-nocobase": "1.6.20",
|
||||
"@nocobase/server": "1.6.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/client": "1.6.6"
|
||||
"@nocobase/client": "1.6.20"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -7,7 +7,7 @@
|
||||
* 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 axios from 'axios';
|
||||
import execa from 'execa';
|
||||
@ -64,7 +64,7 @@ const createDatabase = async () => {
|
||||
if (process.env.DB_DIALECT === 'sqlite') {
|
||||
return 'nocobase';
|
||||
}
|
||||
const db = mockDatabase();
|
||||
const db = await createMockDatabase();
|
||||
const name = `d_${uid()}`;
|
||||
await db.sequelize.query(`CREATE DATABASE ${name}`);
|
||||
await db.close();
|
||||
|
@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@nocobase/auth",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.20",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/actions": "1.6.6",
|
||||
"@nocobase/cache": "1.6.6",
|
||||
"@nocobase/database": "1.6.6",
|
||||
"@nocobase/resourcer": "1.6.6",
|
||||
"@nocobase/utils": "1.6.6",
|
||||
"@nocobase/actions": "1.6.20",
|
||||
"@nocobase/cache": "1.6.20",
|
||||
"@nocobase/database": "1.6.20",
|
||||
"@nocobase/resourcer": "1.6.20",
|
||||
"@nocobase/utils": "1.6.20",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"jsonwebtoken": "^8.5.1"
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/build",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.20",
|
||||
"description": "Library build tool based on rollup.",
|
||||
"main": "lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
@ -17,7 +17,7 @@
|
||||
"@lerna/project": "4.0.0",
|
||||
"@rsbuild/plugin-babel": "^1.0.3",
|
||||
"@rsdoctor/rspack-plugin": "^0.4.8",
|
||||
"@rspack/core": "1.1.1",
|
||||
"@rspack/core": "1.3.2",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/gulp": "^4.0.13",
|
||||
"@types/lerna__package": "5.1.0",
|
||||
|
@ -347,6 +347,7 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou
|
||||
umdNamedDefine: true,
|
||||
},
|
||||
},
|
||||
amd: {},
|
||||
resolve: {
|
||||
tsConfig: path.join(process.cwd(), 'tsconfig.json'),
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.css'],
|
||||
|
2
packages/core/cache/package.json
vendored
2
packages/core/cache/package.json
vendored
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/cache",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.20",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/cli",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.20",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./src/index.js",
|
||||
@ -8,7 +8,7 @@
|
||||
"nocobase": "./bin/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nocobase/app": "1.6.6",
|
||||
"@nocobase/app": "1.6.20",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@umijs/utils": "3.5.20",
|
||||
"chalk": "^4.1.1",
|
||||
@ -25,7 +25,7 @@
|
||||
"tsx": "^4.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/devtools": "1.6.6"
|
||||
"@nocobase/devtools": "1.6.20"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -460,6 +460,16 @@ exports.initEnv = function initEnv() {
|
||||
process.env.SOCKET_PATH = generateGatewayPath();
|
||||
fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { 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 () {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/client",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.20",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "lib/index.js",
|
||||
"module": "es/index.mjs",
|
||||
@ -27,9 +27,9 @@
|
||||
"@formily/reactive-react": "^2.2.27",
|
||||
"@formily/shared": "^2.2.27",
|
||||
"@formily/validator": "^2.2.27",
|
||||
"@nocobase/evaluators": "1.6.6",
|
||||
"@nocobase/sdk": "1.6.6",
|
||||
"@nocobase/utils": "1.6.6",
|
||||
"@nocobase/evaluators": "1.6.20",
|
||||
"@nocobase/sdk": "1.6.20",
|
||||
"@nocobase/utils": "1.6.20",
|
||||
"ahooks": "^3.7.2",
|
||||
"antd": "5.12.8",
|
||||
"antd-style": "3.7.1",
|
||||
|
@ -74,6 +74,7 @@ export const ACLRolesCheckProvider = (props) => {
|
||||
url: 'roles:check',
|
||||
},
|
||||
{
|
||||
manual: !api.auth.token,
|
||||
onSuccess(data) {
|
||||
if (!data?.data?.snippets.includes('ui.*')) {
|
||||
setDesignable(false);
|
||||
|
@ -139,7 +139,19 @@ export class APIClient extends APIClientSDK {
|
||||
if (typeof error?.response?.data === 'string') {
|
||||
const tempElement = document.createElement('div');
|
||||
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 (
|
||||
error?.response?.data?.errors ||
|
||||
|
@ -350,23 +350,9 @@ export class Application {
|
||||
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 = {
|
||||
code: 'LOAD_ERROR',
|
||||
...toError(error),
|
||||
...this.apiClient.toErrMessages(error)?.[0],
|
||||
};
|
||||
console.error(error, this.error);
|
||||
}
|
||||
|
@ -11,10 +11,11 @@ import React, { FC } from 'react';
|
||||
import { MainComponent } from './MainComponent';
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<div>App Error</div>
|
||||
<div>{title}</div>
|
||||
{error?.message}
|
||||
{process.env.__TEST__ && error?.stack}
|
||||
</div>
|
||||
|
@ -63,7 +63,7 @@ export const SchemaInitializerItem = memo(
|
||||
className: className,
|
||||
label: children || compile(title),
|
||||
onClick: (info) => {
|
||||
if (info.key !== name) return;
|
||||
if (disabled || info.key !== name) return;
|
||||
if (closeInitializerMenuWhenClick) {
|
||||
setVisible?.(false);
|
||||
}
|
||||
@ -73,10 +73,10 @@ export const SchemaInitializerItem = memo(
|
||||
children: childrenItems,
|
||||
},
|
||||
];
|
||||
}, [name, style, className, children, title, onClick, icon, childrenItems]);
|
||||
}, [name, disabled, style, className, children, title, onClick, icon, childrenItems]);
|
||||
|
||||
if (items && items.length > 0) {
|
||||
return <SchemaInitializerMenu items={menuItems}></SchemaInitializerMenu>;
|
||||
return <SchemaInitializerMenu disabled={disabled} items={menuItems}></SchemaInitializerMenu>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
|
@ -10,11 +10,11 @@
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
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 { CollectOperators } from './CollectOperators';
|
||||
import { FormBlockProvider } from './FormBlockProvider';
|
||||
import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
|
||||
|
||||
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
||||
const filedSchema = useFieldSchema();
|
||||
@ -35,7 +35,7 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
||||
}}
|
||||
>
|
||||
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
|
||||
<FormBlockProvider name="filter-form" {...props}></FormBlockProvider>
|
||||
<FormBlockProvider name="filter-form" {...props} confirmBeforeClose={false}></FormBlockProvider>
|
||||
</DefaultValueProvider>
|
||||
</ActionBarProvider>
|
||||
</DatePickerProvider>
|
||||
|
@ -167,7 +167,7 @@ export function useCollectValuesToSubmit() {
|
||||
if (parsedValue !== null && parsedValue !== undefined) {
|
||||
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
||||
}
|
||||
} else if (value != null && value !== '') {
|
||||
} else if (value !== '') {
|
||||
assignedValues[key] = value;
|
||||
}
|
||||
});
|
||||
@ -338,7 +338,7 @@ export const useAssociationCreateActionProps = () => {
|
||||
if (parsedValue) {
|
||||
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
||||
}
|
||||
} else if (value != null && value !== '') {
|
||||
} else if (value !== '') {
|
||||
assignedValues[key] = value;
|
||||
}
|
||||
});
|
||||
@ -522,9 +522,11 @@ export const useFilterBlockActionProps = () => {
|
||||
const { doFilter } = useDoFilter();
|
||||
const actionField = useField();
|
||||
actionField.data = actionField.data || {};
|
||||
const form = useForm();
|
||||
|
||||
return {
|
||||
async onClick() {
|
||||
await form.submit();
|
||||
actionField.data.loading = true;
|
||||
await doFilter();
|
||||
actionField.data.loading = false;
|
||||
@ -605,7 +607,7 @@ export const useCustomizeUpdateActionProps = () => {
|
||||
if (parsedValue) {
|
||||
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
||||
}
|
||||
} else if (value != null && value !== '') {
|
||||
} else if (value !== '') {
|
||||
assignedValues[key] = value;
|
||||
}
|
||||
});
|
||||
@ -708,7 +710,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
|
||||
if (parsedValue) {
|
||||
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
||||
}
|
||||
} else if (value != null && value !== '') {
|
||||
} else if (value !== '') {
|
||||
assignedValues[key] = value;
|
||||
}
|
||||
});
|
||||
@ -930,7 +932,7 @@ export const useUpdateActionProps = () => {
|
||||
if (parsedValue) {
|
||||
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
||||
}
|
||||
} else if (value != null && value !== '') {
|
||||
} else if (value !== '') {
|
||||
assignedValues[key] = value;
|
||||
}
|
||||
});
|
||||
|
@ -62,6 +62,12 @@ export class InputFieldInterface extends CollectionFieldInterface {
|
||||
hasDefaultValue = true;
|
||||
properties = {
|
||||
...defaultProps,
|
||||
trim: {
|
||||
type: 'boolean',
|
||||
'x-content': '{{t("Automatically remove heading and tailing spaces")}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
layout: {
|
||||
type: 'void',
|
||||
title: '{{t("Index")}}',
|
||||
|
@ -129,12 +129,12 @@ export const enumType = [
|
||||
label: '{{t("is")}}',
|
||||
value: '$eq',
|
||||
selected: true,
|
||||
schema: { 'x-component': 'Select' },
|
||||
schema: { 'x-component': 'Select', 'x-component-props': { mode: null } },
|
||||
},
|
||||
{
|
||||
label: '{{t("is not")}}',
|
||||
value: '$ne',
|
||||
schema: { 'x-component': 'Select' },
|
||||
schema: { 'x-component': 'Select', 'x-component-props': { mode: null } },
|
||||
},
|
||||
{
|
||||
label: '{{t("is any of")}}',
|
||||
|
@ -31,6 +31,12 @@ export class TextareaFieldInterface extends CollectionFieldInterface {
|
||||
titleUsable = true;
|
||||
properties = {
|
||||
...defaultProps,
|
||||
trim: {
|
||||
type: 'boolean',
|
||||
'x-content': '{{t("Automatically remove heading and tailing spaces")}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
};
|
||||
schemaInitialize(schema: ISchema, { block }) {
|
||||
if (['Table', 'Kanban'].includes(block)) {
|
||||
|
@ -18,7 +18,14 @@ export interface SelectWithTitleProps {
|
||||
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 timerRef = useRef<any>(null);
|
||||
return (
|
||||
@ -36,6 +43,7 @@ export function SelectWithTitle({ title, defaultValue, onChange, options, fieldN
|
||||
>
|
||||
{title}
|
||||
<Select
|
||||
{...others}
|
||||
open={open}
|
||||
data-testid={`select-${title}`}
|
||||
popupMatchSelectWidth={false}
|
||||
|
@ -18,6 +18,7 @@ import { useCollectionFieldUISchema, useIsInNocoBaseRecursionFieldContext } from
|
||||
import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps';
|
||||
import { useCompile, useComponent } from '../../schema-component';
|
||||
import { useIsAllowToSetDefaultValue } from '../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
||||
import { isVariable } from '../../variables/utils/isVariable';
|
||||
import { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider';
|
||||
|
||||
type Props = {
|
||||
@ -102,17 +103,43 @@ const CollectionFieldInternalField = (props) => {
|
||||
const dynamicProps = useDynamicComponentProps(uiSchema?.['x-use-component-props'], props);
|
||||
|
||||
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.
|
||||
// This code is meant to fix this issue.
|
||||
/**
|
||||
* 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.
|
||||
* This code is meant to fix this issue.
|
||||
*/
|
||||
if (fieldSchema['x-read-pretty'] === true && !field.readPretty) {
|
||||
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]);
|
||||
|
||||
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) => {
|
||||
|
@ -48,6 +48,7 @@ interface INocoBaseRecursionFieldProps extends IRecursionFieldProps {
|
||||
* Whether to use Formily Field class - performance will be reduced but provides better compatibility with Formily
|
||||
*/
|
||||
isUseFormilyField?: boolean;
|
||||
parentSchema?: Schema;
|
||||
}
|
||||
|
||||
const CollectionFieldUISchemaContext = React.createContext<CollectionFieldOptions>({});
|
||||
@ -266,6 +267,7 @@ export const NocoBaseRecursionField: ReactFC<INocoBaseRecursionFieldProps> = Rea
|
||||
values,
|
||||
isUseFormilyField = true,
|
||||
uiSchema,
|
||||
parentSchema,
|
||||
} = props;
|
||||
const basePath = useBasePath(props);
|
||||
const newFieldSchemaRef = useRef(null);
|
||||
@ -279,6 +281,14 @@ export const NocoBaseRecursionField: ReactFC<INocoBaseRecursionFieldProps> = Rea
|
||||
|
||||
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 parent = fieldSchema.parent;
|
||||
newFieldSchemaRef.current = new Schema(fieldSchema.toJSON(), parent);
|
||||
|
@ -884,5 +884,7 @@
|
||||
"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.",
|
||||
"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"
|
||||
}
|
||||
|
@ -884,5 +884,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."
|
||||
"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"
|
||||
}
|
||||
|
@ -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 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?",
|
||||
"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."
|
||||
}
|
||||
|
@ -821,5 +821,7 @@
|
||||
"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.",
|
||||
"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
@ -1039,5 +1039,7 @@
|
||||
"If selected, the page will display Tab pages.": "選択されている場合、ページはタブページを表示します。",
|
||||
"If selected, the route will be displayed in the menu.": "選択されている場合、ルートはメニューに表示されます。",
|
||||
"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エディタ」アイコンをクリックしてください"
|
||||
}
|
||||
|
@ -912,5 +912,7 @@
|
||||
"If selected, the page will display Tab pages.": "선택되면 페이지는 탭 페이지를 표시합니다.",
|
||||
"If selected, the route will be displayed in the menu.": "선택되면 라우트는 메뉴에 표시됩니다.",
|
||||
"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 편집기\" 아이콘을 클릭하십시오"
|
||||
}
|
||||
|
@ -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 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?",
|
||||
"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"
|
||||
}
|
||||
|
@ -607,5 +607,7 @@
|
||||
"If selected, the page will display Tab pages.": "Если выбран, страница будет отображать страницы с вкладками.",
|
||||
"If selected, the route will be displayed in the menu.": "Если выбран, маршрут будет отображаться в меню.",
|
||||
"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": "Нажмите на значок \"Редактор пользовательского интерфейса\" в правом верхнем углу, чтобы войти в режим редактора пользовательского интерфейса"
|
||||
}
|
||||
|
@ -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 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?",
|
||||
"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"
|
||||
}
|
||||
|
@ -821,5 +821,7 @@
|
||||
"If selected, the page will display Tab pages.": "Якщо вибрано, сторінка відобразить сторінки з вкладками.",
|
||||
"If selected, the route will be displayed in the menu.": "Якщо вибрано, маршрут буде відображений в меню.",
|
||||
"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": "Натисніть на значок \"Редактор користувацького інтерфейсу\" в правому верхньому куті, щоб увійти в режим редактора користувацького інтерфейсу."
|
||||
}
|
||||
|
@ -258,6 +258,7 @@
|
||||
"Parent collection fields": "父表字段",
|
||||
"Basic": "基本类型",
|
||||
"Single line text": "单行文本",
|
||||
"Automatically remove heading and tailing spaces": "自动去除首尾空白字符",
|
||||
"Long text": "多行文本",
|
||||
"Phone": "手机号码",
|
||||
"Email": "电子邮箱",
|
||||
@ -1080,5 +1081,7 @@
|
||||
"If selected, the page will display Tab pages.": "如果选中,该页面将显示标签页。",
|
||||
"If selected, the route will be displayed in the menu.": "如果选中,该路由将显示在菜单中。",
|
||||
"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": "点击右上角的“界面配置”图标,进入界面配置模式"
|
||||
}
|
||||
|
@ -912,6 +912,7 @@
|
||||
"If selected, the page will display Tab pages.": "如果選中,該頁面將顯示標籤頁。",
|
||||
"If selected, the route will be displayed in the menu.": "如果選中,該路由將顯示在菜單中。",
|
||||
"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": "點擊右上角的 \"介面設定\" 圖示進入介面設定模式"
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,7 @@ test.describe('where list block can be added', () => {
|
||||
await page.getByLabel('schema-initializer-Grid-').nth(1).hover();
|
||||
await page.getByRole('menuitem', { name: 'Role name' }).click();
|
||||
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('Admin')).toBeVisible();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByText('Member')).toBeVisible();
|
||||
|
@ -94,9 +94,13 @@ export const FilterCollectionFieldInternalField: React.FC = (props: Props) => {
|
||||
// @ts-ignore
|
||||
field.dataSource = uiSchema.enum;
|
||||
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]);
|
||||
|
||||
if (!uiSchemaOrigin) return null;
|
||||
|
@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import _ from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useBlockContext, useOpenModeContext } from '../../../../';
|
||||
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
|
||||
@ -45,11 +46,12 @@ export const ellipsisSettingsItem: SchemaSettingsItemType = {
|
||||
tableFieldInstanceList.forEach((fieldInstance) => {
|
||||
fieldInstance.componentProps.ellipsis = checked;
|
||||
});
|
||||
schema['x-component-props']['ellipsis'] = checked;
|
||||
} else {
|
||||
formField.componentProps.ellipsis = checked;
|
||||
}
|
||||
|
||||
_.set(schema, 'x-component-props.ellipsis', checked);
|
||||
|
||||
await dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': schema['x-uid'],
|
||||
|
@ -126,6 +126,10 @@ export const getAllowMultiple = (params?: { title: string }) => {
|
||||
return {
|
||||
name: 'allowMultiple',
|
||||
type: 'switch',
|
||||
useVisible() {
|
||||
const isAssociationField = useIsAssociationField();
|
||||
return isAssociationField;
|
||||
},
|
||||
useComponentProps() {
|
||||
const { t } = useTranslation();
|
||||
const field = useField<Field>();
|
||||
|
@ -14,6 +14,14 @@ import React, { useCallback, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Router } from 'react-router-dom';
|
||||
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 { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
|
||||
import {
|
||||
@ -34,6 +42,8 @@ export const LinkMenuItem = () => {
|
||||
const { urlSchema, paramsSchema } = useURLAndHTMLSchema();
|
||||
const parentRoute = useParentRoute();
|
||||
const { createRoute } = useNocoBaseRoutes();
|
||||
const dm = useDataSourceManager();
|
||||
const cm = useCollectionManager();
|
||||
|
||||
const handleClick = useCallback(async () => {
|
||||
const values = await FormDialog(
|
||||
@ -41,31 +51,35 @@ export const LinkMenuItem = () => {
|
||||
() => {
|
||||
const history = createMemoryHistory();
|
||||
return (
|
||||
<Router location={history.location} navigator={history}>
|
||||
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
properties: {
|
||||
title: {
|
||||
title: t('Menu item title'),
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
icon: {
|
||||
title: t('Icon'),
|
||||
'x-component': 'IconPicker',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
href: urlSchema,
|
||||
params: paramsSchema,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
</SchemaComponentOptions>
|
||||
</Router>
|
||||
<DataSourceManagerProvider dataSourceManager={dm}>
|
||||
<CollectionManagerProvider instance={cm} dataSource={cm?.dataSource?.key}>
|
||||
<Router location={history.location} navigator={history}>
|
||||
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
properties: {
|
||||
title: {
|
||||
title: t('Menu item title'),
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
icon: {
|
||||
title: t('Icon'),
|
||||
'x-component': 'IconPicker',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
href: urlSchema,
|
||||
params: paramsSchema,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
</SchemaComponentOptions>
|
||||
</Router>
|
||||
</CollectionManagerProvider>
|
||||
</DataSourceManagerProvider>
|
||||
);
|
||||
},
|
||||
theme,
|
||||
|
@ -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 }) => {
|
||||
const props = useErrorProps(app, error);
|
||||
const props = getProps(app);
|
||||
return (
|
||||
<div>
|
||||
<Result
|
||||
@ -87,8 +87,9 @@ const AppError: FC<{ error: Error; app: Application }> = observer(
|
||||
transform: translate(0, -50%);
|
||||
`}
|
||||
status="error"
|
||||
title={app.i18n.t('App error')}
|
||||
title={error?.title || app.i18n.t('App error', { ns: 'client' })}
|
||||
subTitle={app.i18n.t(error?.message)}
|
||||
{...props}
|
||||
extra={[
|
||||
<Button type="primary" key="try" onClick={() => window.location.reload()}>
|
||||
{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') {
|
||||
return {
|
||||
status: 'info',
|
||||
|
@ -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]);
|
||||
});
|
||||
});
|
@ -7,13 +7,14 @@
|
||||
* 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 { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header';
|
||||
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 ReactDOM from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ACLRolesCheckProvider,
|
||||
@ -197,12 +198,34 @@ const pageContentStyle: React.CSSProperties = {
|
||||
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 = () => {
|
||||
/* Use the "nb-subpages-slot-without-header-and-side" class name to locate the position of the subpages */
|
||||
return (
|
||||
<div className={`${layoutContentClass} nb-subpages-slot-without-header-and-side`}>
|
||||
<div style={pageContentStyle}>
|
||||
<Outlet />
|
||||
<ShowTipWhenNoPages />
|
||||
</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 { allAccessRoutes } = useAllAccessDesktopRoutes();
|
||||
const location = useLocationNoUpdate();
|
||||
|
||||
const defaultPageUid = getDefaultPageUid(allAccessRoutes);
|
||||
const defaultPageUid = findFirstPageRoute(allAccessRoutes)?.schemaUid;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -718,36 +725,6 @@ export function findRouteBySchemaUid(schemaUid: string, treeArray: any[]) {
|
||||
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 { render: renderInitializer } = useSchemaInitializerRender(menuItemInitializer);
|
||||
|
||||
@ -869,16 +846,17 @@ function findRouteById(id: string, treeArray: any[]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) {
|
||||
export function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) {
|
||||
if (!routes) return;
|
||||
|
||||
for (const route of routes) {
|
||||
for (const route of routes.filter((item) => !item.hideInMenu)) {
|
||||
if (route.type === NocoBaseDesktopRouteType.page) {
|
||||
return route;
|
||||
}
|
||||
|
||||
if (route.children?.length) {
|
||||
return findFirstPageRoute(route.children);
|
||||
if (route.type === NocoBaseDesktopRouteType.group && route.children?.length) {
|
||||
const result = findFirstPageRoute(route.children);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,12 @@ import { ComposedAction } from './types';
|
||||
export const ActionLink: ComposedAction = withDynamicSchemaProps(
|
||||
observer((props: any) => {
|
||||
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' },
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { Modal, ModalProps } from 'antd';
|
||||
import { Modal, ModalProps, Skeleton } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React, { FC, startTransition, useEffect, useState } from 'react';
|
||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||
@ -53,7 +53,6 @@ const ActionModalContent: FC<{ footerNodeName: string; field: any; schema: any }
|
||||
if (!deferredVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NocoBaseRecursionField
|
||||
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(
|
||||
(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 ready = useDelayedVisible(visible, 200); // 200ms 与 Modal 动画时间一致
|
||||
|
||||
return (
|
||||
<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>
|
||||
</TabsContextProvider>
|
||||
</zIndexContext.Provider>
|
||||
|
@ -247,7 +247,6 @@ const InternalAction: React.FC<InternalActionProps> = observer(function Com(prop
|
||||
const aclCtx = useACLActionParamsContext();
|
||||
const { run, element, disabled: disableAction } = useAction?.(actionCallback) || ({} as any);
|
||||
const disabled = form.disabled || field.disabled || field.data?.disabled || propsDisabled || disableAction;
|
||||
|
||||
const buttonStyle = useMemo(() => {
|
||||
return {
|
||||
...style,
|
||||
@ -538,6 +537,7 @@ const RenderButtonInner = observer(
|
||||
Designer: React.ElementType;
|
||||
designerProps: any;
|
||||
title: string;
|
||||
isLink?: boolean;
|
||||
}) => {
|
||||
const {
|
||||
designable,
|
||||
@ -558,6 +558,7 @@ const RenderButtonInner = observer(
|
||||
Designer,
|
||||
designerProps,
|
||||
title,
|
||||
isLink,
|
||||
...others
|
||||
} = props;
|
||||
const debouncedClick = useCallback(
|
||||
@ -582,7 +583,8 @@ const RenderButtonInner = observer(
|
||||
}
|
||||
|
||||
const actionTitle = title || field?.title;
|
||||
|
||||
const { opacity, ...restButtonStyle } = buttonStyle;
|
||||
const linkStyle = isLink && opacity ? { opacity } : undefined;
|
||||
return (
|
||||
<SortableItem
|
||||
role="button"
|
||||
@ -591,15 +593,19 @@ const RenderButtonInner = observer(
|
||||
onMouseEnter={handleMouseEnter}
|
||||
// @ts-ignore
|
||||
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}
|
||||
style={buttonStyle}
|
||||
style={isLink ? restButtonStyle : buttonStyle}
|
||||
onClick={process.env.__E2E__ ? handleButtonClick : debouncedClick} // E2E 中的点击操作都是很快的,如果加上 debounce 会导致 E2E 测试失败
|
||||
component={tarComponent || Button}
|
||||
className={classnames(componentCls, hashId, className, 'nb-action')}
|
||||
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} />
|
||||
</SortableItem>
|
||||
);
|
||||
|
@ -14,6 +14,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useAPIClient, useRequest } from '../../../api-client';
|
||||
import { useCollectionManager } from '../../../data-source/collection';
|
||||
import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord';
|
||||
import { getDataSourceHeaders } from '../../../data-source/utils';
|
||||
import { useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
|
||||
import { useSchemaComponentContext } from '../../hooks';
|
||||
import { AssociationFieldContext } from './context';
|
||||
@ -67,9 +68,11 @@ export const AssociationFieldProvider = observer(
|
||||
if (_.isUndefined(ids) || _.isNil(ids) || _.isNaN(ids)) {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
return api.request({
|
||||
resource: collectionField.target,
|
||||
action: Array.isArray(ids) ? 'list' : 'get',
|
||||
headers: getDataSourceHeaders(cm?.dataSource?.key),
|
||||
params: {
|
||||
filter: {
|
||||
[targetKey]: ids,
|
||||
|
@ -14,22 +14,28 @@ import { uid } from '@formily/shared';
|
||||
import { Space, message } from 'antd';
|
||||
import { isEqual } from 'lodash';
|
||||
import { isFunction } from 'mathjs';
|
||||
import React, { useEffect, useState, useContext } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ClearCollectionFieldContext,
|
||||
NocoBaseRecursionField,
|
||||
RecordProvider,
|
||||
SchemaComponentContext,
|
||||
useAPIClient,
|
||||
useCollectionRecordData,
|
||||
SchemaComponentContext,
|
||||
useCollectionManager_deprecated,
|
||||
} from '../../../';
|
||||
import { Action } from '../action';
|
||||
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||
import { isVariable } from '../../../variables/utils/isVariable';
|
||||
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
|
||||
import { Action } from '../action';
|
||||
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
|
||||
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) => {
|
||||
const schemaComponentCtxValue = useContext(SchemaComponentContext);
|
||||
@ -69,6 +75,11 @@ export const filterAnalyses = (filters): any[] => {
|
||||
return results;
|
||||
};
|
||||
|
||||
function getFieldPath(str) {
|
||||
const lastIndex = str.lastIndexOf('.');
|
||||
return lastIndex === -1 ? str : str.slice(0, lastIndex);
|
||||
}
|
||||
|
||||
const InternalAssociationSelect = observer(
|
||||
(props: AssociationSelectProps) => {
|
||||
const { objectValue = true, addMode: propsAddMode, ...rest } = props;
|
||||
@ -88,6 +99,9 @@ const InternalAssociationSelect = observer(
|
||||
const resource = api.resource(collectionField.target);
|
||||
const recordData = useCollectionRecordData();
|
||||
const schemaComponentCtxValue = useContext(SchemaComponentContext);
|
||||
const { getCollection } = useCollectionManager_deprecated();
|
||||
const associationCollection = getCollection(collectionField.target);
|
||||
const { filterTargetKey } = associationCollection;
|
||||
|
||||
useEffect(() => {
|
||||
const initValue = isVariable(field.value) ? undefined : field.value;
|
||||
@ -100,11 +114,14 @@ const InternalAssociationSelect = observer(
|
||||
//支持深层次子表单
|
||||
onFieldInputValueChange('*', (fieldPath: any) => {
|
||||
const linkageFields = filterAnalyses(field.componentProps?.service?.params?.filter) || [];
|
||||
const linageFieldEntire = getFieldPath(fieldPath.address.entire);
|
||||
const targetFieldEntire = getFieldPath(field.address.entire);
|
||||
if (
|
||||
linkageFields.includes(fieldPath?.props?.name) &&
|
||||
field.value &&
|
||||
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);
|
||||
setInnerValue(null);
|
||||
@ -151,7 +168,6 @@ const InternalAssociationSelect = observer(
|
||||
</div>
|
||||
);
|
||||
};
|
||||
console.log(fieldSchema);
|
||||
return (
|
||||
<div key={fieldSchema.name}>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
@ -160,7 +176,7 @@ const InternalAssociationSelect = observer(
|
||||
{...rest}
|
||||
size={'middle'}
|
||||
objectValue={objectValue}
|
||||
value={value || innerValue}
|
||||
value={removeIfKeyEmpty(value || innerValue, filterTargetKey)}
|
||||
service={service}
|
||||
onChange={(value) => {
|
||||
const val = value?.length !== 0 ? value : null;
|
||||
|
@ -13,6 +13,8 @@ import { FormProvider, connect, createSchemaField, observer, useField, useFieldS
|
||||
import { uid } from '@formily/shared';
|
||||
import { Select as AntdSelect, Input, Space, Spin, Tag } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { css } from '@emotion/css';
|
||||
import { debounce } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient, useCollectionManager_deprecated } from '../../../';
|
||||
@ -152,7 +154,11 @@ const CascadeSelect = connect((props) => {
|
||||
} else {
|
||||
associationField.value = option;
|
||||
}
|
||||
onChange?.(options);
|
||||
if (options.length === 1 && !options[0].value) {
|
||||
onChange?.(null);
|
||||
} else {
|
||||
onChange?.(options);
|
||||
}
|
||||
};
|
||||
|
||||
const onDropdownVisibleChange = async (visible, selectedValue, index) => {
|
||||
@ -238,28 +244,38 @@ export const InternalCascadeSelect = observer(
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { loading, data: formData } = useDataBlockRequest() || {};
|
||||
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(() => {
|
||||
const id = uid();
|
||||
selectForm.addEffects(id, () => {
|
||||
onFormValuesChange((form) => {
|
||||
if (collectionField.interface === 'm2o') {
|
||||
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;
|
||||
});
|
||||
}
|
||||
handleFormValuesChange(form);
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
selectForm.removeEffects(id);
|
||||
// 清除防抖定时器
|
||||
handleFormValuesChange.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -282,6 +298,24 @@ export const InternalCascadeSelect = observer(
|
||||
items: {
|
||||
type: 'void',
|
||||
'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: {
|
||||
sort: {
|
||||
type: 'void',
|
||||
|
@ -256,8 +256,7 @@ export const SubTable: any = observer(
|
||||
{field.editable && (
|
||||
<Space
|
||||
style={{
|
||||
marginTop: '10px',
|
||||
position: field.value?.length ? 'absolute' : 'relative',
|
||||
position: 'relative',
|
||||
bottom: '0',
|
||||
gap: 15,
|
||||
}}
|
||||
|
@ -602,12 +602,6 @@ const InternalNocoBaseTable = React.memo(
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.ant-table-expanded-row-fixed {
|
||||
min-height: ${tableHeight}px;
|
||||
}
|
||||
.ant-table-body {
|
||||
min-height: ${tableHeight}px;
|
||||
}
|
||||
.ant-table-cell {
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ export function useAssociationFieldContext<F extends GeneralField>() {
|
||||
};
|
||||
}
|
||||
|
||||
// 用于获取关系字段请求数据时所需的一些参数
|
||||
export default function useServiceOptions(props) {
|
||||
const { action = 'list', service, useOriginalFilter } = props;
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
@ -54,7 +54,7 @@ describe('CollectionSelect', () => {
|
||||
role="button"
|
||||
>
|
||||
<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
|
||||
class="ant-formily-item-label"
|
||||
@ -191,7 +191,7 @@ describe('CollectionSelect', () => {
|
||||
role="button"
|
||||
>
|
||||
<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
|
||||
class="ant-formily-item-label"
|
||||
|
@ -289,7 +289,7 @@ DatePicker.FilterWithPicker = function FilterWithPicker(props: any) {
|
||||
const [stateProps, setStateProps] = useState(newProps);
|
||||
useEffect(() => {
|
||||
newProps.picker = targetPicker;
|
||||
const dateTimeFormat = getDateTimeFormat(targetPicker, format, showTime, timeFormat);
|
||||
const dateTimeFormat = getDateTimeFormat(targetPicker, targetDateFormat, showTime, timeFormat);
|
||||
newProps.format = dateTimeFormat;
|
||||
setStateProps(newProps);
|
||||
}, [targetPicker]);
|
||||
|
@ -65,7 +65,6 @@ export const DynamicComponent = (props: Props) => {
|
||||
...props.style,
|
||||
},
|
||||
utc: false,
|
||||
underFilter: true,
|
||||
}),
|
||||
name: 'value',
|
||||
'x-read-pretty': false,
|
||||
|
@ -70,6 +70,7 @@ export const FilterItem = observer(
|
||||
className={css`
|
||||
width: 160px;
|
||||
`}
|
||||
showSearch
|
||||
fieldNames={fieldNames}
|
||||
changeOnSelect={false}
|
||||
value={dataIndex}
|
||||
|
@ -92,7 +92,7 @@ export const useGetFilterFieldOptions = () => {
|
||||
|
||||
const getOptions = (fields, depth, usedInVariable?: boolean) => {
|
||||
const options = [];
|
||||
fields.forEach((field) => {
|
||||
fields?.forEach((field) => {
|
||||
const option = field2option(field, depth, usedInVariable);
|
||||
if (option) {
|
||||
options.push(option);
|
||||
|
@ -985,12 +985,11 @@ function useFormItemCollectionField() {
|
||||
|
||||
export function useIsAssociationField() {
|
||||
const collectionField = useFormItemCollectionField();
|
||||
const isAssociationField = ['obo', 'oho', 'o2o', 'o2m', 'm2m', 'm2o', 'updatedBy', 'createdBy', 'mbm'].includes(
|
||||
collectionField?.interface,
|
||||
);
|
||||
const isAssociationField =
|
||||
collectionField &&
|
||||
['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(collectionField.type);
|
||||
return isAssociationField;
|
||||
}
|
||||
|
||||
export function useIsFileField() {
|
||||
const cm = useCollectionManager();
|
||||
const collectionField = useFormItemCollectionField();
|
||||
|
@ -37,6 +37,17 @@ const formItemWrapCss = css`
|
||||
.ant-description-textarea img {
|
||||
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`
|
||||
@ -44,7 +55,7 @@ const formItemLabelCss = css`
|
||||
padding: 0px !important;
|
||||
}
|
||||
> .ant-formily-item-label {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -83,7 +94,6 @@ export const FormItem: any = withDynamicSchemaProps(
|
||||
[formItemLabelCss]: showTitle === false,
|
||||
});
|
||||
}, [showTitle]);
|
||||
|
||||
// 联动规则中的“隐藏保留值”的效果
|
||||
if (field.data?.hidden) {
|
||||
return null;
|
||||
|
@ -86,11 +86,6 @@ const useParseDefaultValue = () => {
|
||||
field &&
|
||||
((isVariable(fieldSchema.default) && field.value == null) || field.value === fieldSchema.default || forceUpdate)
|
||||
) {
|
||||
// 一个变量字符串如果显示出来会比较奇怪
|
||||
if (isVariable(field.value)) {
|
||||
await field.reset({ forceClear: true });
|
||||
}
|
||||
|
||||
field.loading = true;
|
||||
const collectionField = !fieldSchema.name.toString().includes('.') && collection?.getField(fieldSchema.name);
|
||||
|
||||
|
@ -16,8 +16,10 @@ import { ConfigProvider, theme } from 'antd';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useActionContext } from '..';
|
||||
import { useAttach, useComponent } from '../..';
|
||||
import { useApp } from '../../../application';
|
||||
import { getCardItemSchema } from '../../../block-provider';
|
||||
import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider';
|
||||
import { useDataBlockProps } from '../../../data-source';
|
||||
import { useDataBlockRequest } from '../../../data-source/data-block/DataBlockRequestProvider';
|
||||
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
|
||||
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
||||
@ -27,7 +29,6 @@ import { useToken } from '../../../style';
|
||||
import { useLocalVariables, useVariables } from '../../../variables';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
import { useFormBlockHeight } from './hook';
|
||||
import { useApp } from '../../../application';
|
||||
|
||||
export interface FormProps extends IFormLayoutProps {
|
||||
form?: FormilyForm;
|
||||
@ -141,12 +142,15 @@ const WithForm = (props: WithFormProps) => {
|
||||
const linkageRules: any[] =
|
||||
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
|
||||
|
||||
// 关闭弹窗之前,如果有未保存的数据,是否要二次确认
|
||||
const { confirmBeforeClose = true } = useDataBlockProps() || ({} as any);
|
||||
|
||||
useEffect(() => {
|
||||
const id = uid();
|
||||
|
||||
form.addEffects(id, () => {
|
||||
onFormInputChange(() => {
|
||||
setFormValueChanged?.(true);
|
||||
setFormValueChanged?.(confirmBeforeClose);
|
||||
});
|
||||
});
|
||||
|
||||
@ -157,7 +161,7 @@ const WithForm = (props: WithFormProps) => {
|
||||
return () => {
|
||||
form.removeEffects(id);
|
||||
};
|
||||
}, [form, props.disabled, setFormValueChanged]);
|
||||
}, [form, props.disabled, setFormValueChanged, confirmBeforeClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
@ -210,17 +214,19 @@ const WithForm = (props: WithFormProps) => {
|
||||
const WithoutForm = (props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { setFormValueChanged } = useActionContext();
|
||||
// 关闭弹窗之前,如果有未保存的数据,是否要二次确认
|
||||
const { confirmBeforeClose = true } = useDataBlockProps() || ({} as any);
|
||||
const form = useMemo(
|
||||
() =>
|
||||
createForm({
|
||||
disabled: props.disabled,
|
||||
effects() {
|
||||
onFormInputChange((form) => {
|
||||
setFormValueChanged?.(true);
|
||||
setFormValueChanged?.(confirmBeforeClose);
|
||||
});
|
||||
},
|
||||
}),
|
||||
[],
|
||||
[confirmBeforeClose],
|
||||
);
|
||||
return fieldSchema['x-decorator'] === 'FormV2' ? (
|
||||
<FormDecorator form={form} {...props} />
|
||||
|
@ -20,7 +20,6 @@ import { FilterBlockProvider } from '../../../filter-provider/FilterProvider';
|
||||
import {
|
||||
NocoBaseRecursionField,
|
||||
RefreshComponentProvider,
|
||||
useRefreshComponent,
|
||||
useRefreshFieldSchema,
|
||||
} from '../../../formily/NocoBaseRecursionField';
|
||||
import { DndContext, DndContextProps } from '../../common/dnd-context';
|
||||
@ -379,11 +378,9 @@ export const Grid: any = observer(
|
||||
}, [fieldSchema, render, InitializerComponent, showDivider]);
|
||||
|
||||
const refreshFieldSchema = useRefreshFieldSchema();
|
||||
const refreshComponent = useRefreshComponent();
|
||||
const refresh = useCallback(() => {
|
||||
refreshFieldSchema?.();
|
||||
refreshComponent?.();
|
||||
}, [refreshComponent, refreshFieldSchema]);
|
||||
}, [refreshFieldSchema]);
|
||||
|
||||
return (
|
||||
<RefreshComponentProvider refresh={refresh}>
|
||||
|
@ -11,22 +11,40 @@ import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { connect, mapProps, mapReadPretty } from '@formily/react';
|
||||
import { Input as AntdInput } from 'antd';
|
||||
import { InputProps, TextAreaProps } from 'antd/es/input';
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { JSONTextAreaProps, Json } from './Json';
|
||||
import { InputReadPrettyComposed, ReadPretty } from './ReadPretty';
|
||||
|
||||
export { ReadPretty as InputReadPretty } from './ReadPretty';
|
||||
|
||||
type ComposedInput = React.FC<InputProps> & {
|
||||
type ComposedInput = React.FC<NocoBaseInputProps> & {
|
||||
ReadPretty: InputReadPrettyComposed['Input'];
|
||||
TextArea: React.FC<TextAreaProps> & { ReadPretty: InputReadPrettyComposed['TextArea'] };
|
||||
URL: React.FC<InputProps> & { ReadPretty: InputReadPrettyComposed['URL'] };
|
||||
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(
|
||||
connect(
|
||||
AntdInput,
|
||||
InputInner,
|
||||
mapProps((props, field) => {
|
||||
return {
|
||||
...props,
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import React from 'react';
|
||||
import { mockApp } from '@nocobase/client/demo-utils';
|
||||
import { SchemaComponent, Plugin, ISchema } from '@nocobase/client';
|
||||
@ -15,15 +14,25 @@ const schema: ISchema = {
|
||||
'x-decorator': 'FormItem',
|
||||
'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 = () => {
|
||||
return <SchemaComponent schema={schema} />;
|
||||
};
|
||||
|
||||
class DemoPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.router.add('root', { path: '/', Component: Demo })
|
||||
this.app.router.add('root', { path: '/', Component: Demo });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -273,31 +273,43 @@ const InternalPagePopups = (props: { paramsList?: PopupParams[] }) => {
|
||||
);
|
||||
});
|
||||
const schemas = await Promise.all(waitList);
|
||||
const clonedSchemas = schemas.map((schema, index) => {
|
||||
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);
|
||||
const clonedSchemas = await Promise.all(
|
||||
schemas.map(async (schema, index) => {
|
||||
if (_.isEmpty(schema)) {
|
||||
return get404Schema();
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
const params = popupParams[index];
|
||||
|
||||
if (params.puid) {
|
||||
const popupSchema = findSchemaByUid(params.puid, fieldSchema?.root);
|
||||
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) => {
|
||||
const schemaContext = getPopupContextFromActionOrAssociationFieldSchema(schema);
|
||||
let hidden = false;
|
||||
|
@ -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>
|
@ -1,5 +0,0 @@
|
||||
# Page
|
||||
|
||||
可以与 DocumentTitleProvider 搭配使用,将 page title 显示在 document.title 上。
|
||||
|
||||
<code src="./demos/demo1.tsx"></code>
|
@ -27,10 +27,10 @@ export const mapTimeFormat = function () {
|
||||
...props,
|
||||
format,
|
||||
inputReadOnly: true,
|
||||
value: dayjsable(props.value, format),
|
||||
value: dayjsable(props.value, 'HH:mm:ss'),
|
||||
onChange: (value: dayjs.Dayjs | dayjs.Dayjs[]) => {
|
||||
if (onChange) {
|
||||
onChange(formatDayjsValue(value, format) || null);
|
||||
onChange(formatDayjsValue(value, 'HH:mm:ss') || null);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import { DeleteOutlined, DownloadOutlined, InboxOutlined, LoadingOutlined, PlusO
|
||||
import { Field } from '@formily/core';
|
||||
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
|
||||
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 cls from 'classnames';
|
||||
import { saveAs } from 'file-saver';
|
||||
@ -36,6 +37,12 @@ import {
|
||||
import { useStyles } from './style';
|
||||
import type { ComposedUpload, DraggerProps, DraggerV2Props, UploadProps } from './type';
|
||||
|
||||
const LightBoxGlobalStyle = createGlobalStyle`
|
||||
.ReactModal__Overlay.ReactModal__Overlay--after-open {
|
||||
z-index: 3000 !important; // 避免预览图片时被遮挡
|
||||
}
|
||||
`;
|
||||
|
||||
attachmentFileTypes.add({
|
||||
match(file) {
|
||||
return matchMimetype(file, 'image/*');
|
||||
@ -62,34 +69,37 @@ attachmentFileTypes.add({
|
||||
[index, list],
|
||||
);
|
||||
return (
|
||||
<LightBox
|
||||
// discourageDownloads={true}
|
||||
mainSrc={list[index]?.url}
|
||||
nextSrc={list[(index + 1) % list.length]?.url}
|
||||
prevSrc={list[(index + list.length - 1) % list.length]?.url}
|
||||
onCloseRequest={() => onSwitchIndex(null)}
|
||||
onMovePrevRequest={() => onSwitchIndex((index + list.length - 1) % list.length)}
|
||||
onMoveNextRequest={() => onSwitchIndex((index + 1) % list.length)}
|
||||
imageTitle={list[index]?.title}
|
||||
toolbarButtons={[
|
||||
<button
|
||||
key={'preview-img'}
|
||||
style={{ fontSize: 22, background: 'none', lineHeight: 1 }}
|
||||
type="button"
|
||||
aria-label="Download"
|
||||
title="Download"
|
||||
className="ril-zoom-in ril__toolbarItemChild ril__builtinButton"
|
||||
onClick={onDownload}
|
||||
>
|
||||
<DownloadOutlined />
|
||||
</button>,
|
||||
]}
|
||||
/>
|
||||
<>
|
||||
<LightBoxGlobalStyle />
|
||||
<LightBox
|
||||
// discourageDownloads={true}
|
||||
mainSrc={list[index]?.url}
|
||||
nextSrc={list[(index + 1) % list.length]?.url}
|
||||
prevSrc={list[(index + list.length - 1) % list.length]?.url}
|
||||
onCloseRequest={() => onSwitchIndex(null)}
|
||||
onMovePrevRequest={() => onSwitchIndex((index + list.length - 1) % list.length)}
|
||||
onMoveNextRequest={() => onSwitchIndex((index + 1) % list.length)}
|
||||
imageTitle={list[index]?.title}
|
||||
toolbarButtons={[
|
||||
<button
|
||||
key={'preview-img'}
|
||||
style={{ fontSize: 22, background: 'none', lineHeight: 1 }}
|
||||
type="button"
|
||||
aria-label="Download"
|
||||
title="Download"
|
||||
className="ril-zoom-in ril__toolbarItemChild ril__builtinButton"
|
||||
onClick={onDownload}
|
||||
>
|
||||
<DownloadOutlined />
|
||||
</button>,
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const iframePreviewSupportedTypes = ['application/pdf', 'audio/*', 'image/*', 'video/*', 'text/*'];
|
||||
const iframePreviewSupportedTypes = ['application/pdf', 'audio/*', 'image/*', 'video/*', 'text/plain'];
|
||||
|
||||
function IframePreviewer({ index, list, onSwitchIndex }) {
|
||||
const { t } = useTranslation();
|
||||
|
@ -26,12 +26,13 @@ import { Json } from '../input';
|
||||
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
|
||||
|
||||
type ParseOptions = {
|
||||
defaultTypeOnNull?: string;
|
||||
stringToDate?: boolean;
|
||||
};
|
||||
|
||||
function parseValue(value: any, options: ParseOptions = {}): string | string[] {
|
||||
if (value == null) {
|
||||
return 'null';
|
||||
return options.defaultTypeOnNull ?? 'null';
|
||||
}
|
||||
const type = typeof value;
|
||||
if (type === 'string') {
|
||||
|
@ -7,9 +7,9 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Button, Input } from 'antd';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { VariableSelect } from './VariableSelect';
|
||||
|
||||
// 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}
|
||||
onInsert={onInsert}
|
||||
changeOnSelect={changeOnSelect}
|
||||
disabled={others.disabled}
|
||||
/>
|
||||
</Button.Group>
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useForm } from '@formily/react';
|
||||
import { Space, theme } from 'antd';
|
||||
import type { CascaderProps, DefaultOptionType } from 'antd/lib/cascader';
|
||||
import useInputStyle from 'antd/es/input/style';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
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[]>();
|
||||
for (const option of options) {
|
||||
map.set(option[fieldNames.value], [option[fieldNames.label]]);
|
||||
@ -220,10 +221,24 @@ function useVariablesFromValue(value: string, delimiters: [string, string] = ['{
|
||||
}, [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 { scope, onChange, changeOnSelect, style, fieldNames, delimiters = ['{{', '}}'], addonBefore } = props;
|
||||
const value = typeof props.value === 'string' ? props.value : props.value == null ? '' : props.value.toString();
|
||||
const { scope, changeOnSelect, style, fieldNames, delimiters = ['{{', '}}'], addonBefore, trim = true } = props;
|
||||
const value =
|
||||
typeof props.value === 'string' ? props.value : props.value == null ? '' : (props.value as any).toString();
|
||||
const variables = useVariablesFromValue(value, delimiters);
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
const [options, setOptions] = useState([]);
|
||||
@ -241,6 +256,14 @@ export function TextArea(props) {
|
||||
const { token } = theme.useToken();
|
||||
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(() => {
|
||||
preloadOptions(scope, variables)
|
||||
.then((preloaded) => {
|
||||
@ -324,9 +347,9 @@ export function TextArea(props) {
|
||||
|
||||
setChanged(true);
|
||||
setRange(getCurrentRange(current));
|
||||
onChange(getValue(current, delimiters));
|
||||
onChange(current);
|
||||
},
|
||||
[keyLabelMap, onChange, range, delimitersString],
|
||||
[keyLabelMap, onChange, range],
|
||||
);
|
||||
|
||||
const onInput = useCallback(
|
||||
@ -336,9 +359,9 @@ export function TextArea(props) {
|
||||
}
|
||||
setChanged(true);
|
||||
setRange(getCurrentRange(currentTarget));
|
||||
onChange(getValue(currentTarget, delimiters));
|
||||
onChange(currentTarget);
|
||||
},
|
||||
[ime, onChange, delimitersString],
|
||||
[ime, onChange],
|
||||
);
|
||||
|
||||
const onBlur = useCallback(function ({ currentTarget }) {
|
||||
@ -360,9 +383,9 @@ export function TextArea(props) {
|
||||
setIME(false);
|
||||
setChanged(true);
|
||||
setRange(getCurrentRange(currentTarget));
|
||||
onChange(getValue(currentTarget, delimiters));
|
||||
onChange(currentTarget);
|
||||
},
|
||||
[onChange, delimitersString],
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const onPaste = useCallback(
|
||||
@ -393,9 +416,9 @@ export function TextArea(props) {
|
||||
setChanged(true);
|
||||
pasteHTML(ev.currentTarget, sanitizedHTML);
|
||||
setRange(getCurrentRange(ev.currentTarget));
|
||||
onChange(getValue(ev.currentTarget, delimiters));
|
||||
onChange(ev.currentTarget);
|
||||
},
|
||||
[onChange, delimitersString],
|
||||
[onChange],
|
||||
);
|
||||
const disabled = props.disabled || form.disabled;
|
||||
return wrapSSR(
|
||||
|
@ -96,7 +96,6 @@ export const conditionAnalyses = async (
|
||||
) => {
|
||||
const type = Object.keys(ruleGroup)[0] || '$and';
|
||||
const conditions = ruleGroup[type];
|
||||
|
||||
let results = conditions.map(async (condition) => {
|
||||
if ('$and' in condition || '$or' in condition) {
|
||||
return await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic);
|
||||
@ -147,7 +146,10 @@ export const conditionAnalyses = async (
|
||||
if (type === '$and') {
|
||||
return every(results, (v) => v);
|
||||
} else {
|
||||
return some(results, (v) => v);
|
||||
if (results.length) {
|
||||
return some(results, (v) => v);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { createForm } from '@formily/core';
|
||||
import { Schema } from '@formily/react';
|
||||
import { Schema, useFieldSchema } from '@formily/react';
|
||||
import { Spin } from 'antd';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { useRemoteCollectionManagerLoading } from '../../collection-manager/CollectionManagerProvider';
|
||||
@ -62,6 +62,7 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
|
||||
});
|
||||
const NotFoundComponent = useComponent(NotFoundPage);
|
||||
const collectionManagerLoading = useRemoteCollectionManagerLoading();
|
||||
const parentSchema = useFieldSchema();
|
||||
|
||||
if (collectionManagerLoading || loading || hidden) {
|
||||
return <Spin style={{ width: '100%', marginTop: 20 }} delay={LOADING_DELAY} />;
|
||||
@ -73,10 +74,20 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
|
||||
}
|
||||
|
||||
return noForm ? (
|
||||
<SchemaComponent components={components} scope={scope} schema={schemaTransform(schema || {})} />
|
||||
<SchemaComponent
|
||||
components={components}
|
||||
scope={scope}
|
||||
schema={schemaTransform(schema || {})}
|
||||
parentSchema={parentSchema}
|
||||
/>
|
||||
) : (
|
||||
<FormProvider form={form}>
|
||||
<SchemaComponent components={components} scope={scope} schema={schemaTransform(schema || {})} />
|
||||
<SchemaComponent
|
||||
components={components}
|
||||
scope={scope}
|
||||
schema={schemaTransform(schema || {})}
|
||||
parentSchema={parentSchema}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
@ -28,6 +28,7 @@ function toSchema(schema?: any) {
|
||||
properties: {
|
||||
[schema.name]: schema,
|
||||
},
|
||||
name: `p_${schema.name}`,
|
||||
});
|
||||
}
|
||||
return new Schema(schema);
|
||||
@ -52,58 +53,65 @@ interface DistributedProps {
|
||||
*/
|
||||
export const SchemaComponentOnChangeContext = createContext<SchemaComponentOnChange>({ onChange: _.noop });
|
||||
|
||||
const RecursionSchemaComponent = memo((props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => {
|
||||
const { components, scope, schema: _schema, distributed, onChange: _onChange, ...others } = props;
|
||||
const ctx = useContext(SchemaComponentContext);
|
||||
const schema = useMemo(() => toSchema(_schema), [_schema]);
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
...ctx,
|
||||
distributed: ctx.distributed == false ? false : distributed,
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
refresh: ctx.refresh || _.noop,
|
||||
}),
|
||||
[ctx, distributed],
|
||||
);
|
||||
const RecursionSchemaComponent = memo(
|
||||
(props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps & { parentSchema?: Schema }) => {
|
||||
const { components, scope, schema: _schema, distributed, onChange: _onChange, parentSchema, ...others } = props;
|
||||
const ctx = useContext(SchemaComponentContext);
|
||||
const schema = useMemo(() => toSchema(_schema), [_schema]);
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
...ctx,
|
||||
distributed: ctx.distributed == false ? false : distributed,
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
refresh: ctx.refresh || _.noop,
|
||||
}),
|
||||
[ctx, distributed],
|
||||
);
|
||||
|
||||
const { onChange: onChangeFromContext } = useContext(SchemaComponentOnChangeContext);
|
||||
const { onChange: onChangeFromContext } = useContext(SchemaComponentOnChangeContext);
|
||||
|
||||
const onChangeValue = useMemo(
|
||||
() => ({
|
||||
onChange: () => {
|
||||
_onChange?.(schema);
|
||||
onChangeFromContext?.();
|
||||
},
|
||||
}),
|
||||
[_onChange, onChangeFromContext, schema],
|
||||
);
|
||||
const onChangeValue = useMemo(
|
||||
() => ({
|
||||
onChange: () => {
|
||||
_onChange?.(schema);
|
||||
onChangeFromContext?.();
|
||||
},
|
||||
}),
|
||||
[_onChange, onChangeFromContext, schema],
|
||||
);
|
||||
|
||||
return (
|
||||
<SchemaComponentOnChangeContext.Provider value={onChangeValue}>
|
||||
<SchemaComponentContext.Provider value={value}>
|
||||
<SchemaComponentOptions inherit components={components} scope={scope}>
|
||||
<NocoBaseRecursionField {...others} schema={schema} isUseFormilyField />
|
||||
</SchemaComponentOptions>
|
||||
</SchemaComponentContext.Provider>
|
||||
</SchemaComponentOnChangeContext.Provider>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<SchemaComponentOnChangeContext.Provider value={onChangeValue}>
|
||||
<SchemaComponentContext.Provider value={value}>
|
||||
<SchemaComponentOptions inherit components={components} scope={scope}>
|
||||
<NocoBaseRecursionField {...others} schema={schema} isUseFormilyField parentSchema={parentSchema} />
|
||||
</SchemaComponentOptions>
|
||||
</SchemaComponentContext.Provider>
|
||||
</SchemaComponentOnChangeContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
RecursionSchemaComponent.displayName = 'RecursionSchemaComponent';
|
||||
|
||||
const MemoizedSchemaComponent = memo((props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => {
|
||||
const { schema, ...others } = props;
|
||||
const s = useMemoizedSchema(schema);
|
||||
return <RecursionSchemaComponent {...others} schema={s} />;
|
||||
});
|
||||
const MemoizedSchemaComponent = memo(
|
||||
(props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps & { parentSchema?: Schema }) => {
|
||||
const { schema, parentSchema, ...others } = props;
|
||||
const s = useMemoizedSchema(schema);
|
||||
return <RecursionSchemaComponent {...others} schema={s} parentSchema={parentSchema} />;
|
||||
},
|
||||
);
|
||||
|
||||
MemoizedSchemaComponent.displayName = 'MemoizedSchemaComponent';
|
||||
|
||||
export const SchemaComponent = memo(
|
||||
(
|
||||
props: (ISchemaFieldProps | IRecursionFieldProps) & { memoized?: boolean } & SchemaComponentOnChange &
|
||||
props: (ISchemaFieldProps | IRecursionFieldProps) & {
|
||||
memoized?: boolean;
|
||||
parentSchema?: Schema;
|
||||
} & SchemaComponentOnChange &
|
||||
DistributedProps,
|
||||
) => {
|
||||
const { memoized, ...others } = props;
|
||||
|
@ -37,7 +37,9 @@ const getPageHeaderHeight = (disablePageHeader, enablePageTabs, hidePageTitle, t
|
||||
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 {
|
||||
if (enablePageTabs) {
|
||||
return (
|
||||
@ -140,12 +142,12 @@ export const useDataBlockHeight = (options?: UseDataBlockHeightOptions) => {
|
||||
const { heightMode, height, title, titleHeight } = heightProps || {};
|
||||
|
||||
const blockHeaderHeight = title ? titleHeight : 0;
|
||||
|
||||
if (!heightProps?.heightMode || heightMode === HeightMode.DEFAULT) {
|
||||
return;
|
||||
}
|
||||
if (heightMode === HeightMode.FULL_HEIGHT) {
|
||||
let res = window.innerHeight - pageFullScreenHeight;
|
||||
console.log(res);
|
||||
if (options?.removeBlockHeaderHeight) {
|
||||
res = res - blockHeaderHeight;
|
||||
}
|
||||
|
@ -268,10 +268,12 @@ function FinallyButton({
|
||||
const { getCollection } = useCollectionManager_deprecated();
|
||||
const aclCtx = useACLActionParamsContext();
|
||||
const buttonStyle = useMemo(() => {
|
||||
const shouldApplyOpacity = designable && (field?.data?.hidden || !aclCtx);
|
||||
const opacityValue = componentType !== 'link' ? (shouldApplyOpacity ? 0.1 : 1) : 1;
|
||||
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 (!linkageFromForm) {
|
||||
|
@ -470,7 +470,6 @@ export const useFilterFormItemInitializerFields = (options?: any) => {
|
||||
'x-collection-field': `${name}.${field.name}`,
|
||||
'x-component-props': {
|
||||
utc: false,
|
||||
underFilter: true,
|
||||
},
|
||||
};
|
||||
if (isAssocField(field)) {
|
||||
@ -485,7 +484,7 @@ export const useFilterFormItemInitializerFields = (options?: any) => {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-use-decorator-props': 'useFormItemProps',
|
||||
'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 = {
|
||||
@ -580,7 +579,7 @@ const associationFieldToMenu = (
|
||||
interface: field.interface,
|
||||
},
|
||||
'x-component': 'CollectionField',
|
||||
'x-component-props': { utc: false, underFilter: true },
|
||||
'x-component-props': { utc: false },
|
||||
'x-read-pretty': false,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': `${collectionName}.${schemaName}`,
|
||||
@ -697,7 +696,7 @@ export const useFilterInheritsFormItemInitializerFields = (options?) => {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'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'],
|
||||
};
|
||||
return {
|
||||
@ -728,7 +727,7 @@ export const useCustomFormItemInitializerFields = (options?: any) => {
|
||||
const remove = useRemoveGridFormItem();
|
||||
return currentFields
|
||||
?.filter((field) => {
|
||||
return field?.interface && field.interface !== 'snapshot' && field.type !== 'sequence';
|
||||
return !field.inherit && field?.interface && field.interface !== 'snapshot' && field.type !== 'sequence';
|
||||
})
|
||||
?.map((field) => {
|
||||
const interfaceConfig = getInterface(field.interface);
|
||||
|
@ -568,13 +568,14 @@ export interface SchemaSettingsSelectItemProps
|
||||
extends Omit<SchemaSettingsItemProps, 'onChange' | 'onClick'>,
|
||||
Omit<SelectWithTitleProps, 'title' | 'defaultValue'> {
|
||||
value?: SelectWithTitleProps['defaultValue'];
|
||||
optionRender?: (option: any, info: { index: number }) => React.ReactNode;
|
||||
}
|
||||
export const SchemaSettingsSelectItem: FC<SchemaSettingsSelectItemProps> = (props) => {
|
||||
const { title, options, value, onChange, ...others } = props;
|
||||
const { title, options, value, onChange, optionRender, ...others } = props;
|
||||
|
||||
return (
|
||||
<SchemaSettingsItem title={title} {...others}>
|
||||
<SelectWithTitle {...{ title, defaultValue: value, onChange, options }} />
|
||||
<SelectWithTitle {...{ title, defaultValue: value, onChange, options, optionRender }} />
|
||||
</SchemaSettingsItem>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,11 @@ export const useCurrentUserContext = () => {
|
||||
return useContext(CurrentUserContext);
|
||||
};
|
||||
|
||||
export const useIsLoggedIn = () => {
|
||||
const ctx = useContext(CurrentUserContext);
|
||||
return !!ctx?.data?.data;
|
||||
};
|
||||
|
||||
export const useCurrentRoles = () => {
|
||||
const { allowAnonymous } = useACLRoleContext();
|
||||
const { data } = useCurrentUserContext();
|
||||
@ -39,14 +44,18 @@ export const useCurrentRoles = () => {
|
||||
|
||||
export const CurrentUserProvider = (props) => {
|
||||
const api = useAPIClient();
|
||||
const result = useRequest<any>(() =>
|
||||
api
|
||||
.request({
|
||||
url: '/auth:check',
|
||||
skipNotify: true,
|
||||
skipAuth: true,
|
||||
})
|
||||
.then((res) => res?.data),
|
||||
const result = useRequest<any>(
|
||||
() =>
|
||||
api
|
||||
.request({
|
||||
url: '/auth:check',
|
||||
skipNotify: true,
|
||||
skipAuth: true,
|
||||
})
|
||||
.then((res) => res?.data),
|
||||
{
|
||||
manual: !api.auth.token,
|
||||
},
|
||||
);
|
||||
const { render } = useAppSpin();
|
||||
|
||||
|
@ -18,7 +18,6 @@ import { getDataSourceHeaders } from '../data-source/utils';
|
||||
import { useCompile } from '../schema-component';
|
||||
import useBuiltInVariables from './hooks/useBuiltinVariables';
|
||||
import { VariableOption, VariablesContextType } from './types';
|
||||
import { cacheLazyLoadedValues, getCachedLazyLoadedValues } from './utils/cacheLazyLoadedValues';
|
||||
import { filterEmptyValues } from './utils/filterEmptyValues';
|
||||
import { getAction } from './utils/getAction';
|
||||
import { getPath } from './utils/getPath';
|
||||
@ -144,14 +143,13 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
|
||||
.then((data) => {
|
||||
clearRequested(url);
|
||||
const value = data.data.data;
|
||||
cacheLazyLoadedValues(item, currentVariablePath, value);
|
||||
return value;
|
||||
});
|
||||
stashRequested(url, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return getCachedLazyLoadedValues(item, currentVariablePath) || item?.[key];
|
||||
return item?.[key];
|
||||
});
|
||||
current = removeThroughCollectionFields(_.flatten(await Promise.all(result)), associationField);
|
||||
} else if (
|
||||
@ -180,17 +178,9 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
|
||||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
current = removeThroughCollectionFields(
|
||||
getCachedLazyLoadedValues(current, currentVariablePath) || getValuesByPath(current, key),
|
||||
associationField,
|
||||
);
|
||||
current = removeThroughCollectionFields(getValuesByPath(current, key), associationField);
|
||||
}
|
||||
|
||||
if (associationField?.target) {
|
||||
@ -359,13 +349,8 @@ export default VariablesProvider;
|
||||
function shouldToRequest(value, variableCtx: Record<string, any>, variablePath: string) {
|
||||
let result = false;
|
||||
|
||||
if (getCachedLazyLoadedValues(variableCtx, variablePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// value may be a reactive object, using untracked to avoid unexpected autorun
|
||||
untracked(() => {
|
||||
// fix https://nocobase.height.app/T-2502
|
||||
// Compatible with `xxx to many` and `xxx to one` subform fields and subtable fields
|
||||
if (JSON.stringify(value) === '[{}]' || JSON.stringify(value) === '{}') {
|
||||
result = true;
|
||||
|
@ -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];
|
||||
};
|
@ -7,7 +7,7 @@
|
||||
* 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 isVariable = (str: unknown) => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-nocobase-app",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.20",
|
||||
"main": "src/index.js",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@nocobase/data-source-manager",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.20",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/actions": "1.6.6",
|
||||
"@nocobase/cache": "1.6.6",
|
||||
"@nocobase/database": "1.6.6",
|
||||
"@nocobase/resourcer": "1.6.6",
|
||||
"@nocobase/utils": "1.6.6",
|
||||
"@nocobase/actions": "1.6.20",
|
||||
"@nocobase/cache": "1.6.20",
|
||||
"@nocobase/database": "1.6.20",
|
||||
"@nocobase/resourcer": "1.6.20",
|
||||
"@nocobase/utils": "1.6.20",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"jsonwebtoken": "^8.5.1"
|
||||
},
|
||||
|
@ -7,10 +7,10 @@
|
||||
* 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 { createMockDatabase, createMockServer, mockDatabase, supertest } from '@nocobase/test';
|
||||
import { vi } from 'vitest';
|
||||
import { SequelizeDataSource } from '../sequelize-data-source';
|
||||
|
||||
describe('example', () => {
|
||||
test.skip('case1', async () => {
|
||||
@ -41,7 +41,7 @@ describe('example', () => {
|
||||
name: 'test2',
|
||||
});
|
||||
|
||||
const database = mockDatabase({
|
||||
const database = await createMockDatabase({
|
||||
tablePrefix: 'ds1_',
|
||||
});
|
||||
await database.clean({ drop: true });
|
||||
@ -82,7 +82,7 @@ describe('example', () => {
|
||||
name: 'update-filter',
|
||||
});
|
||||
|
||||
const database = mockDatabase({
|
||||
const database = await createMockDatabase({
|
||||
tablePrefix: 'ds1_',
|
||||
});
|
||||
|
||||
@ -128,7 +128,7 @@ describe('example', () => {
|
||||
name: 'update-filter',
|
||||
});
|
||||
|
||||
const database = mockDatabase({
|
||||
const database = await createMockDatabase({
|
||||
tablePrefix: 'ds1_',
|
||||
});
|
||||
|
||||
@ -198,7 +198,7 @@ describe('example', () => {
|
||||
// it should be called on main datasource
|
||||
expect(hook).toBeCalledTimes(1);
|
||||
|
||||
const database = mockDatabase({
|
||||
const database = await createMockDatabase({
|
||||
tablePrefix: 'ds1_',
|
||||
});
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
* 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 bodyParser from 'koa-bodyparser';
|
||||
import supertest from 'supertest';
|
||||
@ -24,7 +24,7 @@ describe('example', () => {
|
||||
await next();
|
||||
});
|
||||
app.use(dsm.middleware());
|
||||
const database = mockDatabase({
|
||||
const database = await createMockDatabase({
|
||||
tablePrefix: 'ds1_',
|
||||
});
|
||||
await database.clean({ drop: true });
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@nocobase/database",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.20",
|
||||
"description": "",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@nocobase/logger": "1.6.6",
|
||||
"@nocobase/utils": "1.6.6",
|
||||
"@nocobase/logger": "1.6.20",
|
||||
"@nocobase/utils": "1.6.20",
|
||||
"async-mutex": "^0.3.2",
|
||||
"chalk": "^4.1.1",
|
||||
"cron-parser": "4.4.0",
|
||||
|
@ -7,13 +7,13 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Database } from '../../database';
|
||||
import { mockDatabase } from '../index';
|
||||
import { Database, createMockDatabase } from '@nocobase/database';
|
||||
|
||||
describe('association references', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
db = await createMockDatabase();
|
||||
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
@ -7,13 +7,13 @@
|
||||
* 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 () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
db = await createMockDatabase();
|
||||
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
@ -7,14 +7,13 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Database } from '../database';
|
||||
import { mockDatabase } from './index';
|
||||
import { createMockDatabase, Database } from '@nocobase/database';
|
||||
|
||||
describe.skipIf(process.env['DB_DIALECT'] === 'sqlite')('collection', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase({
|
||||
db = await createMockDatabase({
|
||||
logging: console.log,
|
||||
});
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user