diff --git a/.github/workflows/manual-merge.yml b/.github/workflows/manual-merge.yml index 061765def4..70bcbeaa5d 100644 --- a/.github/workflows/manual-merge.yml +++ b/.github/workflows/manual-merge.yml @@ -65,6 +65,10 @@ jobs: run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ steps.app-token.outputs.token }} + - name: Set user + run: | + git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' + git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' - name: Checkout uses: actions/checkout@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ffad129fff..62920885dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,8 +17,11 @@ jobs: publish-npm: needs: get-plugins runs-on: ubuntu-latest - container: node:18 steps: + - name: Set Node.js 20 + uses: actions/setup-node@v3 + with: + node-version: 20 - name: Get info id: get-info shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 6417e84c92..6efc7690cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,406 @@ 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.5](https://github.com/nocobase/nocobase/compare/v1.6.4...v1.6.5) - 2025-03-17 + +### 🚀 Improvements + +- **[File manager]** Simplify file URL generating logic and API ([#6472](https://github.com/nocobase/nocobase/pull/6472)) by @mytharcher + +- **[File storage: S3(Pro)]** Change to a simple way to generate file URL by @mytharcher + +- **[Backup manager]** Allow restore backup between pre release and release version of the same version by @gchust + +### 🐛 Bug Fixes + +- **[client]** + - rich text field not clearing data on submission ([#6486](https://github.com/nocobase/nocobase/pull/6486)) by @katherinehhh + + - The color of the icons in the upper right corner of the page does not change with the theme ([#6482](https://github.com/nocobase/nocobase/pull/6482)) by @zhangzhonghe + + - Clicking the reset button on the filter form cannot clear the filtering conditions of the grid card block ([#6475](https://github.com/nocobase/nocobase/pull/6475)) by @zhangzhonghe + +- **[Workflow: Manual node]** + - Fix migration ([#6484](https://github.com/nocobase/nocobase/pull/6484)) by @mytharcher + + - Change migration name to ensure rerun ([#6487](https://github.com/nocobase/nocobase/pull/6487)) by @mytharcher + + - Fix workflow title field in filter ([#6480](https://github.com/nocobase/nocobase/pull/6480)) by @mytharcher + + - Fix migration error when id column is not exists ([#6470](https://github.com/nocobase/nocobase/pull/6470)) by @chenos + + - Avoid collection synchronized from fields ([#6478](https://github.com/nocobase/nocobase/pull/6478)) by @mytharcher + +- **[Workflow: Aggregate node]** Fix round on null result ([#6473](https://github.com/nocobase/nocobase/pull/6473)) by @mytharcher + +- **[Workflow]** Don't count tasks when workflow deleted ([#6474](https://github.com/nocobase/nocobase/pull/6474)) by @mytharcher + +- **[Backup manager]** Not able to start server when missing default backup settings by @gchust + +- **[Workflow: Approval]** + - Fix file association field error in process form by @mytharcher + + - Fix tasks count based on hooks by @mytharcher + +## [v1.6.4](https://github.com/nocobase/nocobase/compare/v1.6.3...v1.6.4) - 2025-03-14 + +### 🎉 New Features + +- **[client]** Cascade Selection Component Add Data Scope Setting ([#6386](https://github.com/nocobase/nocobase/pull/6386)) by @Cyx649312038 + +### 🚀 Improvements + +- **[utils]** Move `md5` to utils ([#6468](https://github.com/nocobase/nocobase/pull/6468)) by @mytharcher + +### 🐛 Bug Fixes + +- **[client]** In the tree block, when unchecked, the data in the data block is not being cleared ([#6460](https://github.com/nocobase/nocobase/pull/6460)) by @zhangzhonghe + +- **[File manager]** Unable to delete files stored in S3. ([#6467](https://github.com/nocobase/nocobase/pull/6467)) by @chenos + +- **[Workflow]** Remove bind workflow settings button from data picker ([#6455](https://github.com/nocobase/nocobase/pull/6455)) by @mytharcher + +- **[File storage: S3(Pro)]** Resolve issue with inaccessible S3 Pro signed URLs by @chenos + +- **[Workflow: Approval]** Avoid page crash when no applicant in approval process table by @mytharcher + +## [v1.6.3](https://github.com/nocobase/nocobase/compare/v1.6.2...v1.6.3) - 2025-03-13 + +### 🎉 New Features + +- **[WeCom]** Allows setting a custom tooltip for the sign-in button by @2013xile + +### 🐛 Bug Fixes + +- **[client]** + - Fix special character in image URL caused not showing ([#6459](https://github.com/nocobase/nocobase/pull/6459)) by @mytharcher + + - incorrect page number when adding data after subtable page size change ([#6437](https://github.com/nocobase/nocobase/pull/6437)) by @katherinehhh + + - The logo style is inconsistent with the previous one ([#6444](https://github.com/nocobase/nocobase/pull/6444)) by @zhangzhonghe + +- **[Workflow: Manual node]** Fix error thrown in migration ([#6445](https://github.com/nocobase/nocobase/pull/6445)) by @mytharcher + +- **[Data visualization]** Removed fields appear when adding custom filter fields ([#6450](https://github.com/nocobase/nocobase/pull/6450)) by @2013xile + +- **[File manager]** Fix a few issues of file manager ([#6436](https://github.com/nocobase/nocobase/pull/6436)) by @mytharcher + +- **[Action: Custom request]** custom request server-side permission validation error ([#6438](https://github.com/nocobase/nocobase/pull/6438)) by @katherinehhh + +- **[Data source manager]** switching data source in role management does not load corresponding collections ([#6431](https://github.com/nocobase/nocobase/pull/6431)) by @katherinehhh + +- **[Action: Batch edit]** Fix workflow can not be triggered in bulk edit submission ([#6440](https://github.com/nocobase/nocobase/pull/6440)) by @mytharcher + +- **[Workflow: Custom action event]** Remove `only` in E2E test case by @mytharcher + +- **[Workflow: Approval]** + - Fix association data not showing in approval form by @mytharcher + + - Fix error thrown when approve on external data source by @mytharcher + +## [v1.6.2](https://github.com/nocobase/nocobase/compare/v1.6.1...v1.6.2) - 2025-03-12 + +### 🐛 Bug Fixes + +- **[client]** date field range selection excludes the max date ([#6418](https://github.com/nocobase/nocobase/pull/6418)) by @katherinehhh + +- **[Notification: In-app message]** Avoid wrong receivers configuration query all users ([#6424](https://github.com/nocobase/nocobase/pull/6424)) by @sheldon66 + +- **[Workflow: Manual node]** + - Fix migration which missed table prefix and schema logic ([#6425](https://github.com/nocobase/nocobase/pull/6425)) by @mytharcher + + - Change version limit of migration to `<1.7.0` ([#6430](https://github.com/nocobase/nocobase/pull/6430)) by @mytharcher + +## [v1.6.1](https://github.com/nocobase/nocobase/compare/v1.6.0...v1.6.1) - 2025-03-11 + +### 🐛 Bug Fixes + +- **[client]** + - When using the '$anyOf' operator, the linkage rule is invalid ([#6415](https://github.com/nocobase/nocobase/pull/6415)) by @zhangzhonghe + + - Data not updating in popup windows opened via Link buttons ([#6411](https://github.com/nocobase/nocobase/pull/6411)) by @zhangzhonghe + + - multi-select field value changes and option loss when deleting subtable records ([#6405](https://github.com/nocobase/nocobase/pull/6405)) by @katherinehhh + +- **[Notification: In-app message]** Differentiate the in-app message list background color from the message cards to enhance visual hierarchy and readability. ([#6417](https://github.com/nocobase/nocobase/pull/6417)) by @sheldon66 + +## [v1.6.0](https://github.com/nocobase/nocobase/compare/v1.5.25...v1.6.0) - 2025-03-11 + +## New Features + +### Cluster Mode + +The Enterprise edition supports cluster mode deployment via relevant plugins. When the application runs in cluster mode, it can leverage multiple instances and multi-core processing to improve its performance in handling concurrent access. + +![Cluster Mode Screenshot](https://static-docs.nocobase.com/20241231010814.png) + +Reference: [Deployment - Cluster Mode](https://docs.nocobase.com/welcome/getting-started/deployment/cluster-mode) + +### Password Policy + +A password policy is established for all users, including rules for password complexity, validity periods, and login security strategies, along with the management of locked accounts. + +![Password Policy Screenshot](https://static-docs.nocobase.com/202412281329313.png) + +Reference: [Password Policy](https://docs.nocobase.com/handbook/password-policy) + +### Token Policy + +The Token Security Policy is a function configuration designed to protect system security and enhance user experience. It includes three main configuration items: "session validity," "token effective period," and "expired token refresh limit." + +![Token Policy Screenshot](https://static-docs.nocobase.com/20250105111821-2025-01-05-11-18-24.png) + +Reference: [Token Policy](https://docs.nocobase.com/handbook/token-policy) + +### IP Restriction + +NocoBase allows administrators to set up an IP allowlist or blacklist for user access to restrict unauthorized external network connections or block known malicious IP addresses, thereby reducing security risks. It also supports querying access denial logs to identify risky IPs. + +![IP Restriction Screenshot](https://static-docs.nocobase.com/2025-01-23-10-07-34-20250123100733.png) + +Reference: [IP Restriction](https://docs.nocobase.com/handbook/IP-restriction) + +### Environment Variables + +Centralized configuration and management of environment variables and secrets are provided for sensitive data storage, configuration reuse, environment isolation, and more. + +![Environment Variables Screenshot](https://static-docs.nocobase.com/1ee6c3fa09533b19f4d6038f53b06006.png) + +Reference: [Environment Variables](https://docs.nocobase.com/handbook/environment-variables) + +### Release Management + +This feature allows you to migrate application configurations from one environment to another. + +![Migration Manager Screenshot](https://static-docs.nocobase.com/20250107105005.png) + +Reference: [Release Management](https://docs.nocobase.com/handbook/release-management) + +### Route Management + +- **Menu Data Separated and Renamed to Routes**: The menu data has been decoupled from the UI Schema and renamed as "routes." It is divided into two tables, desktopRoutes and mobileRoutes, which correspond to desktop and mobile routes respectively. +- **Frontend Menu Optimization with Collapse and Responsive Support**: The frontend menu now supports collapse functionality and responsive design for an improved user experience. + +![Route Management Screenshot](https://static-docs.nocobase.com/20250107115449.png) + +Reference: [Routes](https://docs.nocobase.com/handbook/routes) + +### Roles and Permissions + +- Supports configuration of action action permissions. + ![Roles and Permissions Screenshot 1](https://static-docs.nocobase.com/b0a7905d9fd4beaaf21592b1f56fe752.png) +- Supports configuration of tab permissions. + +![Roles and Permissions Screenshot 2](https://static-docs.nocobase.com/4fd3a5144a2301638b9f24b033d33add.png) + +### User Management + +Supports customization of user profile forms. + +![User Management Screenshot](https://static-docs.nocobase.com/171e5a4c61033afb237c9ae1a3d89000.png) + +### Workflow + +An entry for the workflow to-do center has been added to the global toolbar, providing real-time notifications for manual nodes and pending approval tasks. + +![Workflow Screenshot](https://static-docs.nocobase.com/855c58536f9fd29ae353dd19b3aff73f.png) + +### Workflow: Custom Action Events + +Supports triggering custom action events both globally and in batch actions. + +![Custom Action Events Screenshot](https://static-docs.nocobase.com/106ae1296d180718799eb6d7f423805c.png) + +### Workflow: Approval + +- Supports transferring approval responsibilities and adding extra approvers. + ![Approval Form Screenshot](https://static-docs.nocobase.com/20241226232013.png) +- Allows approvers to modify the application content when submitting an approval. + ![Approval Modification Screenshot](https://static-docs.nocobase.com/20241226232124.png) +- Supports configuration of a basic information block within the approval interface. +- Optimizes the style and interaction of initiating approvals and viewing pending tasks, with these improvements also integrated into the global process to-do center. + ![Approval To-do Center Screenshot](https://static-docs.nocobase.com/20250310161203.png) +- No longer distinguishes the location where the approval is initiated; the approval center block can both initiate and process all approvals. + +### Workflow: JSON Variable Mapping Node + +A new dedicated node has been added to map JSON data from upstream node results into variables. + +![JSON Variable Mapping Node Screenshot](https://static-docs.nocobase.com/20250113173635.png) + +Reference: [JSON Variable Mapping](https://docs.nocobase.com/handbook/workflow/nodes/json-variable-mapping) + +### Capability Enhancements and Plugin Examples + + +| Extension | Plugin Example | +| --------------------------------- | --------------------------------------------------------------- | +| Data Source Custom Preset Fields | @nocobase-sample/plugin-data-source-main-custom-preset-fields | +| Calendar Register Color Field | @nocobase-sample/plugin-calendar-register-color-field | +| Calendar Register Title Field | @nocobase-sample/plugin-calendar-register-title-field | +| Formula Register Expression Field | @nocobase-sample/plugin-field-formula-register-expression-field | +| Kanban Register Group Field | @nocobase-sample/plugin-kanban-register-group-field | +| Register Filter Operator | @nocobase-sample/plugin-register-filter-operator | +| File Storage Extension | @nocobase-sample/plugin-file-storage-demo | + +## Breaking Changes + +### Update to Token Policy + +In version 1.6, a new [Token Policy](https://docs.nocobase.com/handbook/token-policy) was introduced. When authentication fails, a 401 error will be returned along with a redirection to the login page. This behavior differs from previous versions. To bypass the check, refer to the following examples: + +Frontend Request: + +```javascript +useRequest({ + url: '/test', + skipAuth: true, +}); + +api.request({ + url: '/test', + skipAuth: true, +}); +``` + +Backend Middleware: + +```javascript +class PluginMiddlewareExampleServer extends plugin { + middlewareExample = (ctx, next) => { + if (ctx.path === '/path/to') { + ctx.skipAuthCheck = true; + } + await next(); + }; + async load() { + this.app.dataSourceManager.afterAddDataSource((dataSource) => { + dataSource.resourceManager.use(this.middlewareExample, { + before: 'auth', + }); + }); + } +} +``` + +### Unit Test Function agent.login Changed from Synchronous to Asynchronous + +Due to several asynchronous operations required in the authentication process, the test function login is now asynchronous. Example: + +```TypeScript +import { createMockServer } from '@nocobase/test'; + +describe('my db suite', () => { + let app; + let agent; + + beforeEach(async () => { + app = await createMockServer({ + registerActions: true, + acl: true, + plugins: ['users', 'auth', 'acl'], + }); + agent = await app.agent().login(1); + }); + + test('case1', async () => { + await agent.get('/examples'); + await agent.get('/examples'); + await agent.resource('examples').create(); + }); +}); +``` + +### New Extension API for User Center Settings Items + +API: + +```TypeScript +type UserCenterSettingsItemOptions = SchemaSettingsItemType & { aclSnippet?: string }; + +class Application { + addUserCenterSettingsItem(options: UserCenterSettingsItemOptions); +} +``` + +Example: + +```javascript +class PluginUserCenterSettingsExampleClient extends plugin { + async load() { + this.app.addUserCenterSettingsItem({ + name: 'nickName', + Component: NickName, + sort: 0, + }); + } +} +``` + +## [v1.5.25](https://github.com/nocobase/nocobase/compare/v1.5.24...v1.5.25) - 2025-03-09 + +### 🐛 Bug Fixes + +- **[server]** Incorrect browser cache after running `yarn start` command ([#6394](https://github.com/nocobase/nocobase/pull/6394)) by @gchust + +- **[Workflow: Approval]** Avoid wrong assignees configuration query all users by @mytharcher + +- **[WeCom]** fix login prompt link and dingtalk login error by @chenzhizdt + +## [v1.5.24](https://github.com/nocobase/nocobase/compare/v1.5.23...v1.5.24) - 2025-03-07 + +### 🎉 New Features + +- **[Data visualization]** Support NULLS sorting in chart queries ([#6383](https://github.com/nocobase/nocobase/pull/6383)) by @2013xile + +### 🚀 Improvements + +- **[Workflow]** Allow skip to trigger collection workflow in database event ([#6379](https://github.com/nocobase/nocobase/pull/6379)) by @mytharcher + +### 🐛 Bug Fixes + +- **[Action: Import records Pro]** Use additional option to determine whether to trigger workflow or not by @mytharcher + +- **[Action: Export records Pro]** pro export action missing sort params by @katherinehhh + +## [v1.5.23](https://github.com/nocobase/nocobase/compare/v1.5.22...v1.5.23) - 2025-03-06 + +### 🐛 Bug Fixes + +- **[client]** + - timezone-related issue causing one hour less in date picker ([#6359](https://github.com/nocobase/nocobase/pull/6359)) by @katherinehhh + + - missing sortable setting for inherited collection fields ([#6372](https://github.com/nocobase/nocobase/pull/6372)) by @katherinehhh + +## [v1.5.22](https://github.com/nocobase/nocobase/compare/v1.5.21...v1.5.22) - 2025-03-06 + +### 🚀 Improvements + +- **[client]** Add debounce handling to buttons ([#6351](https://github.com/nocobase/nocobase/pull/6351)) by @Cyx649312038 + +### 🐛 Bug Fixes + +- **[database]** Fix error when retrieving relation collection records if the source key in relation fields is a numeric string ([#6360](https://github.com/nocobase/nocobase/pull/6360)) by @2013xile + +## [v1.5.21](https://github.com/nocobase/nocobase/compare/v1.5.20...v1.5.21) - 2025-03-05 + +### 🚀 Improvements + +- **[Workflow]** Lazy load job result for better performance ([#6344](https://github.com/nocobase/nocobase/pull/6344)) by @mytharcher + +- **[Workflow: Aggregate node]** Add round process for aggregated number based on double type ([#6358](https://github.com/nocobase/nocobase/pull/6358)) by @mytharcher + +### 🐛 Bug Fixes + +- **[client]** + - subform components not aligning with main form when label is hidden ([#6357](https://github.com/nocobase/nocobase/pull/6357)) by @katherinehhh + + - association block not rendering in popup within collection inheritance ([#6303](https://github.com/nocobase/nocobase/pull/6303)) by @katherinehhh + + - Fix error thrown when creating file collection ([#6363](https://github.com/nocobase/nocobase/pull/6363)) by @mytharcher + +- **[Workflow]** Fix acl for getting job ([#6352](https://github.com/nocobase/nocobase/pull/6352)) by @mytharcher + ## [v1.5.20](https://github.com/nocobase/nocobase/compare/v1.5.19...v1.5.20) - 2025-03-03 ### 🐛 Bug Fixes diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index d85841fa7b..50631289f8 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -5,6 +5,403 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 +## [v1.6.5](https://github.com/nocobase/nocobase/compare/v1.6.4...v1.6.5) - 2025-03-17 + +### 🚀 优化 + +- **[文件管理器]** 简化生成文件 URL 的逻辑和 API ([#6472](https://github.com/nocobase/nocobase/pull/6472)) by @mytharcher + +- **[文件存储:S3 (Pro)]** 优化生成文件 URL 的方法 by @mytharcher + +- **[备份管理器]** 允许在相同版本的预发布和发布版本之间恢复备份 by @gchust + +### 🐛 修复 + +- **[client]** + - 富文本字段清空后提交时数据未删除 ([#6486](https://github.com/nocobase/nocobase/pull/6486)) by @katherinehhh + + - 页面右上角图标的颜色不会随主题变化 ([#6482](https://github.com/nocobase/nocobase/pull/6482)) by @zhangzhonghe + + - 点击筛选表单的重置按钮无法清除网格卡片区块的筛选条件 ([#6475](https://github.com/nocobase/nocobase/pull/6475)) by @zhangzhonghe + +- **[工作流:人工处理节点]** + - 修复迁移脚本 ([#6484](https://github.com/nocobase/nocobase/pull/6484)) by @mytharcher + + - 修改迁移脚本确保执行 ([#6487](https://github.com/nocobase/nocobase/pull/6487)) by @mytharcher + + - 修复区块的筛选组件中工作流标题项 ([#6480](https://github.com/nocobase/nocobase/pull/6480)) by @mytharcher + + - 修复 id 列不存在时迁移脚本报错 ([#6470](https://github.com/nocobase/nocobase/pull/6470)) by @chenos + + - 避免历史表被关系字段同步出来 ([#6478](https://github.com/nocobase/nocobase/pull/6478)) by @mytharcher + +- **[工作流:聚合查询节点]** 修复对聚合结果为 null 时取整报错 ([#6473](https://github.com/nocobase/nocobase/pull/6473)) by @mytharcher + +- **[工作流]** 不统计已删除的工作流的待办 ([#6474](https://github.com/nocobase/nocobase/pull/6474)) by @mytharcher + +- **[备份管理器]** 默认的备份设置不存在时服务器无法启动 by @gchust + +- **[工作流:审批]** + - 修复审批表单中文件字段报错问题 by @mytharcher + + - 基于钩子事件修复待办任务数量 by @mytharcher + +## [v1.6.4](https://github.com/nocobase/nocobase/compare/v1.6.3...v1.6.4) - 2025-03-14 + +### 🎉 新特性 + +- **[client]** 级联选择组件添加数据范围设置 ([#6386](https://github.com/nocobase/nocobase/pull/6386)) by @Cyx649312038 + +### 🚀 优化 + +- **[utils]** 将 `md5` 方法移到通用包 ([#6468](https://github.com/nocobase/nocobase/pull/6468)) by @mytharcher + +### 🐛 修复 + +- **[client]** 在树区块中,取消选中时,数据区块的数据没有被清空 ([#6460](https://github.com/nocobase/nocobase/pull/6460)) by @zhangzhonghe + +- **[文件管理器]** 无法删除 s3 文件存储的文件 ([#6467](https://github.com/nocobase/nocobase/pull/6467)) by @chenos + +- **[工作流]** 在数据选择器中移除绑定工作流的配置按钮 ([#6455](https://github.com/nocobase/nocobase/pull/6455)) by @mytharcher + +- **[文件存储:S3 (Pro)]** 修复 s3 pro 的签名 url 无法访问的问题 by @chenos + +- **[工作流:审批]** 避免审批流程表格中由于没有发起人时的页面崩溃 by @mytharcher + +## [v1.6.3](https://github.com/nocobase/nocobase/compare/v1.6.2...v1.6.3) - 2025-03-13 + +### 🎉 新特性 + +- **[企业微信]** 支持自定义登录按钮提示 by @2013xile + +### 🐛 修复 + +- **[client]** + - 修复图片中特殊字符导致不显示的问题 ([#6459](https://github.com/nocobase/nocobase/pull/6459)) by @mytharcher + + - 子表格切换分页数后新增数据页码显示错误 ([#6437](https://github.com/nocobase/nocobase/pull/6437)) by @katherinehhh + + - Logo 的样式与之前的不一致 ([#6444](https://github.com/nocobase/nocobase/pull/6444)) by @zhangzhonghe + +- **[工作流:人工处理节点]** 修复迁移脚本报错 ([#6445](https://github.com/nocobase/nocobase/pull/6445)) by @mytharcher + +- **[数据可视化]** 添加自定义筛选字段时会出现已移除字段 ([#6450](https://github.com/nocobase/nocobase/pull/6450)) by @2013xile + +- **[文件管理器]** 修复文件管理一些问题 ([#6436](https://github.com/nocobase/nocobase/pull/6436)) by @mytharcher + +- **[操作:自定义请求]** 自定义请求的服务端权限校验错误 ([#6438](https://github.com/nocobase/nocobase/pull/6438)) by @katherinehhh + +- **[数据源管理]** 角色管理中切换数据源没有加载对应数据表 ([#6431](https://github.com/nocobase/nocobase/pull/6431)) by @katherinehhh + +- **[操作:批量编辑]** 修复批量编辑提交时未能触发工作流的问题 ([#6440](https://github.com/nocobase/nocobase/pull/6440)) by @mytharcher + +- **[工作流:自定义操作事件]** 移除 E2E 测试中的 `only` by @mytharcher + +- **[工作流:审批]** + - 修复审批表单中关系数据未展示的问题 by @mytharcher + + - 修复外部数据源审批时的报错 by @mytharcher + +## [v1.6.2](https://github.com/nocobase/nocobase/compare/v1.6.1...v1.6.2) - 2025-03-12 + +### 🐛 修复 + +- **[client]** 表单日期字段日期范围,最大日期可选范围少一天 ([#6418](https://github.com/nocobase/nocobase/pull/6418)) by @katherinehhh + +- **[通知:站内信]** 避免错误的接收人配置导致查询出全部用户 ([#6424](https://github.com/nocobase/nocobase/pull/6424)) by @sheldon66 + +- **[工作流:人工处理节点]** + - 修复遗漏表前缀和 schema 的迁移脚本 ([#6425](https://github.com/nocobase/nocobase/pull/6425)) by @mytharcher + + - 调整迁移脚本版本范围限制为 `<1.7.0` ([#6430](https://github.com/nocobase/nocobase/pull/6430)) by @mytharcher + +## [v1.6.1](https://github.com/nocobase/nocobase/compare/v1.6.0...v1.6.1) - 2025-03-11 + +### 🐛 修复 + +- **[client]** + - 使用“$anyOf”操作符时,联动规则无效 ([#6415](https://github.com/nocobase/nocobase/pull/6415)) by @zhangzhonghe + + - 使用链接按钮打开的弹窗,数据不更新 ([#6411](https://github.com/nocobase/nocobase/pull/6411)) by @zhangzhonghe + + - 子表格删除记录的时候多选字段值错误且选项缺失 ([#6405](https://github.com/nocobase/nocobase/pull/6405)) by @katherinehhh + +- **[通知:站内信]** 在站内信列表中,将背景颜色与消息卡片的颜色区分开,以提升视觉层次感和可读性。 ([#6417](https://github.com/nocobase/nocobase/pull/6417)) by @sheldon66 + +## [v1.6.0](https://github.com/nocobase/nocobase/compare/v1.5.25...v1.6.0) - 2025-03-11 + +## 新特性 + +### 集群模式 + +企业版可通过相关插件支持集群模式部署,应用以集群模式运行时,可以通过多个实例和使用多核模式来提高应用的对并发访问处理的性能。 + +![20241231010814](https://static-docs.nocobase.com/20241231010814.png) + +参考文档:[集群部署](https://docs-cn.nocobase.com/welcome/getting-started/deployment/cluster-mode) + +### 密码策略 + +为所有用户设置密码规则,密码有效期和密码登录安全策略,管理锁定用户。 + +![](https://static-docs.nocobase.com/202412281329313.png) + +参考文档:[密码策略和用户锁定](https://docs-cn.nocobase.com/handbook/password-policy) + +### Token 安全策略 + +Token 安全策略是一种用于保护系统安全和体验的功能配置。它包括了三个主要配置项:“会话有效期”、“Token 有效周期” 和 “过期 Token 刷新时限” 。 + +![20250105111821-2025-01-05-11-18-24](https://static-docs.nocobase.com/20250105111821-2025-01-05-11-18-24.png) + +参考文档:[Token 安全策略](https://docs-cn.nocobase.com/handbook/token-policy) + +### IP 限制 + +NocoBase 支持管理员对用户访问 IP 设置白名单或黑名单,以限制未授权的外部网络连接或阻止已知的恶意 IP 地址,降低安全风险。同时支持管理员查询访问拒绝日志,识别风险 IP。 + +![2025-01-23-10-07-34-20250123100733](https://static-docs.nocobase.com/2025-01-23-10-07-34-20250123100733.png) + +参考文档:[IP 限制](https://docs-cn.nocobase.com/handbook/IP-restriction) + +### 变量和密钥 + +集中配置和管理环境变量和密钥,用于敏感数据存储、配置数据重用、环境配置隔离等。 + +![1ee6c3fa09533b19f4d6038f53b06006.png](https://static-docs.nocobase.com/1ee6c3fa09533b19f4d6038f53b06006.png) + +参考文档:[变量和密钥](https://docs-cn.nocobase.com/handbook/environment-variables) + +### 迁移管理 + +用于将应用配置从一个应用环境迁移到另一个应用环境。 + +![20250107105005](https://static-docs.nocobase.com/20250107105005.png) + +参考文档:[迁移管理](https://docs-cn.nocobase.com/handbook/migration-manager)[发布管理](https://docs-cn.nocobase.com/handbook/release-management) + +### 路由管理 + +* **菜单数据独立并改名为路由**:菜单数据从 UI Schema 中拆分出来,改名为**路由**,分为 `desktopRoutes` 和 `mobileRoutes` 两张表,分别对应桌面端路由和移动端路由。 +* **菜单前端优化,支持折叠与响应式**:菜单在前端实现了**折叠**与**响应式**适配,提升了使用体验。 + +![20250107115449](https://static-docs.nocobase.com/20250107115449.png) + +参考文档:[路由管理](https://docs-cn.nocobase.com/handbook/routes) + +### 角色和权限 + +* 支持配置更多的操作按钮权限,包括弹窗、链接、扫码、触发工作流 + ![b0a7905d9fd4beaaf21592b1f56fe752.png](https://static-docs.nocobase.com/b0a7905d9fd4beaaf21592b1f56fe752.png) +* 支持配置标签页权限 + + ![4fd3a5144a2301638b9f24b033d33add.png](https://static-docs.nocobase.com/4fd3a5144a2301638b9f24b033d33add.png) + +### 用户管理 + +支持配置用户个人资料表单 + +![171e5a4c61033afb237c9ae1a3d89000.png](https://static-docs.nocobase.com/171e5a4c61033afb237c9ae1a3d89000.png) + +### 工作流 + +在全局工具栏中增加流程待办中心入口,并实时提示人工节点、审批的相关待办任务数量。 + +![855c58536f9fd29ae353dd19b3aff73f.png](https://static-docs.nocobase.com/855c58536f9fd29ae353dd19b3aff73f.png) + +### 工作流:自定义操作事件 + +支持全局和批量数据触发自定义操作事件。 + +![106ae1296d180718799eb6d7f423805c.png](https://static-docs.nocobase.com/106ae1296d180718799eb6d7f423805c.png) + +### 工作流:审批 + +* 支持转签、加签。![审批节点_界面配置_操作表单区块](https://static-docs.nocobase.com/20241226232013.png) +* 支持审批人在提交审批时修改申请内容。![审批节点_界面配置_操作表单_修改审批内容字段](https://static-docs.nocobase.com/20241226232124.png) +* 支持在审批界面中配置审批基础信息区块。 +* 优化审批发起和待办区块的样式和交互,同时也在全局的流程待办中心中内置。![待办中心-审批](https://static-docs.nocobase.com/20250310161203.png) +* 不再区分发起审批的位置,审批中心区块可以发起和处理所有审批。 + +### 工作流:JSON 变量映射节点 + +新增用于将上游节点结果中的 JSON 数据映射为变量的专用节点。 + +![创建节点](https://static-docs.nocobase.com/20250113173635.png) + +参考文档:[JSON 变量映射](https://docs-cn.nocobase.com/handbook/workflow/nodes/json-variable-mapping) + +### 扩展能力提升及插件示例 + + +| 扩展项 | 插件示例 | +| ---------------------- | --------------------------------------------------------------- | +| 数据表预置字段扩展 | @nocobase-sample/plugin-data-source-main-custom-preset-fields | +| 日历颜色字段可选项扩展 | @nocobase-sample/plugin-calendar-register-color-field | +| 日历标题字段可选项扩展 | @nocobase-sample/plugin-calendar-register-title-field | +| 公式可选项字段扩展 | @nocobase-sample/plugin-field-formula-register-expression-field | +| 看板分组字段扩展 | @nocobase-sample/plugin-kanban-register-group-field | +| 筛选操作符扩展 | @nocobase-sample/plugin-register-filter-operator | +| 文件存储扩展 | @nocobase-sample/plugin-file-storage-demo | + +## 不兼容变更 + +### Token 安全策略更新 + +1.6 版本新增了 [Token 安全策略](https://docs-cn.nocobase.com/handbook/token-policy),Auth 认证检查未通过时,将返回 401 错误并跳转至登录页。此行为与之前版本有所不同。如需跳过检查,可参考以下示例进行处理: + +前端请求 + +```javascript +useRequest({ + url: '/test', + skipAuth: true, +}); + +api.request({ + url: '/test', + skipAuth: true, +}); +``` + +后端中间件 + +```javascript +class PluginMiddlewareExampleServer extends plugin { + middlewareExample = (ctx, next) => { + if (ctx.path === '/path/to') { + ctx.skipAuthCheck = true; + } + await next(); + }; + async load() { + this.app.dataSourceManager.afterAddDataSource((dataSource) => { + dataSource.resourceManager.use(this.middlewareExample, { + before: 'auth', + }); + }); + } +} +``` + +### 单元测试函数 agent.login 由同步改为异步 + +由于认证流程需要进行一些异步操作,测试函数 login 改为异步, 示例: + +```TypeScript +import { createMockServer } from '@nocobase/test'; + +describe('my db suite', () => { + let app; + let agent; + + beforeEach(async () => { + app = await createMockServer({ + registerActions: true, + acl: true, + plugins: ['users', 'auth', 'acl'], + }); + agent = await app.agent().login(1); + }); + + test('case1', async () => { + await agent.get('/examples'); + await agent.get('/examples'); + await agent.resource('examples').create(); + }); +}); +``` + +### 提供全新的用户中心设置项的扩展 API + +API + +```ts +type UserCenterSettingsItemOptions = SchemaSettingsItemType & { aclSnippet?: string }; + +class Application { + addUserCenterSettingsItem(options: UserCenterSettingsItemOptions); +} +``` + +示例 + +```javascript +class PluginUserCenterSettingsExampleClient extends plugin { + async load() { + this.app.addUserCenterSettingsItem({ + name: 'nickName', + Component: NickName, + sort: 0, + }); + } +} +``` + +## [v1.5.25](https://github.com/nocobase/nocobase/compare/v1.5.24...v1.5.25) - 2025-03-09 + +### 🐛 修复 + +- **[server]** `yarn start` 启动服务器后前端缓存未刷新 ([#6394](https://github.com/nocobase/nocobase/pull/6394)) by @gchust + +- **[工作流:审批]** 避免错误的审批人配置导致查询出全部用户 by @mytharcher + +- **[企业微信]** 修复登录提示链接和钉钉登录错误 by @chenzhizdt + +## [v1.5.24](https://github.com/nocobase/nocobase/compare/v1.5.23...v1.5.24) - 2025-03-07 + +### 🎉 新特性 + +- **[数据可视化]** 支持在图表查询中设置 NULLS 排序 ([#6383](https://github.com/nocobase/nocobase/pull/6383)) by @2013xile + +### 🚀 优化 + +- **[工作流]** 支持在数据表事件中不触发工作流 ([#6379](https://github.com/nocobase/nocobase/pull/6379)) by @mytharcher + +### 🐛 修复 + +- **[操作:导入记录 Pro]** 使用额外的选项来觉得是否在导入时触发工作流的数据表事件 by @mytharcher + +- **[操作:导出记录 Pro]** pro 导出按钮导出数据时缺失sort 参数 by @katherinehhh + +## [v1.5.23](https://github.com/nocobase/nocobase/compare/v1.5.22...v1.5.23) - 2025-03-06 + +### 🐛 修复 + +- **[client]** + - 日期组件缺陷,选择的日期时间会少一个小时 ([#6359](https://github.com/nocobase/nocobase/pull/6359)) by @katherinehhh + + - 继承父表的字段在表格中缺少排序设置项 ([#6372](https://github.com/nocobase/nocobase/pull/6372)) by @katherinehhh + +## [v1.5.22](https://github.com/nocobase/nocobase/compare/v1.5.21...v1.5.22) - 2025-03-06 + +### 🚀 优化 + +- **[client]** 按钮添加防双击处理 ([#6351](https://github.com/nocobase/nocobase/pull/6351)) by @Cyx649312038 + +### 🐛 修复 + +- **[database]** 修复当关系字段的源表标识字段值为数字型字符串时,获取关系数据记录报错的问题 ([#6360](https://github.com/nocobase/nocobase/pull/6360)) by @2013xile + +## [v1.5.21](https://github.com/nocobase/nocobase/compare/v1.5.20...v1.5.21) - 2025-03-05 + +### 🚀 优化 + +- **[工作流]** 后置节点结果加载以提升执行记录画布性能 ([#6344](https://github.com/nocobase/nocobase/pull/6344)) by @mytharcher + +- **[工作流:聚合查询节点]** 对聚合后的数字进行小数四舍五入的处理 ([#6358](https://github.com/nocobase/nocobase/pull/6358)) by @mytharcher + +### 🐛 修复 + +- **[client]** + - 子表单隐藏字段标题时字段组件与主表单中的组件未对齐 ([#6357](https://github.com/nocobase/nocobase/pull/6357)) by @katherinehhh + + - 数据表继承模型中关系区块在弹窗中未显示 ([#6303](https://github.com/nocobase/nocobase/pull/6303)) by @katherinehhh + + - 修复创建文件表时的报错 ([#6363](https://github.com/nocobase/nocobase/pull/6363)) by @mytharcher + +- **[工作流]** 修复加载节点结果的权限问题 ([#6352](https://github.com/nocobase/nocobase/pull/6352)) by @mytharcher + ## [v1.5.20](https://github.com/nocobase/nocobase/compare/v1.5.19...v1.5.20) - 2025-03-03 ### 🐛 修复 diff --git a/docker/nocobase-full/nocobase.conf b/docker/nocobase-full/nocobase.conf index 61ddc70335..7de9bbb630 100644 --- a/docker/nocobase-full/nocobase.conf +++ b/docker/nocobase-full/nocobase.conf @@ -17,7 +17,7 @@ server { server_name _; root /app/nocobase/packages/app/client/dist; index index.html; - client_max_body_size 20M; + client_max_body_size 0; access_log /var/log/nginx/nocobase.log apm; diff --git a/docker/nocobase/nocobase.conf b/docker/nocobase/nocobase.conf index 7c0c4d2c41..d178440bf9 100644 --- a/docker/nocobase/nocobase.conf +++ b/docker/nocobase/nocobase.conf @@ -17,7 +17,7 @@ server { server_name _; root /app/nocobase/node_modules/@nocobase/app/dist/client; index index.html; - client_max_body_size 1000M; + client_max_body_size 0; access_log /var/log/nginx/nocobase.log apm; gzip on; diff --git a/lerna.json b/lerna.json index ff199d7f8c..6086a88cb5 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.6.0-alpha.29", + "version": "1.7.0-alpha.3", "npmClient": "yarn", "useWorkspaces": true, "npmClientArgs": ["--ignore-engines"], diff --git a/package.json b/package.json index 02fefd9951..b530c0c628 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,9 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "nwsapi": "2.2.7", - "antd": "5.12.8", + "antd": "5.24.2", + "@formily/antd-v5": "1.2.3", + "dayjs": "1.11.13", "@ant-design/icons": "^5.6.1" }, "config": { @@ -93,4 +95,4 @@ "yarn": "1.22.19" }, "dependencies": {} -} +} \ No newline at end of file diff --git a/packages/core/acl/package.json b/packages/core/acl/package.json index 3892df86d4..fc9a4d1e1b 100644 --- a/packages/core/acl/package.json +++ b/packages/core/acl/package.json @@ -1,13 +1,13 @@ { "name": "@nocobase/acl", - "version": "1.6.0-alpha.29", + "version": "1.7.0-alpha.3", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/resourcer": "1.6.0-alpha.29", - "@nocobase/utils": "1.6.0-alpha.29", + "@nocobase/resourcer": "1.7.0-alpha.3", + "@nocobase/utils": "1.7.0-alpha.3", "minimatch": "^5.1.1" }, "repository": { diff --git a/packages/core/acl/src/__tests__/acl-role.test.ts b/packages/core/acl/src/__tests__/acl-role.test.ts new file mode 100644 index 0000000000..88b914361e --- /dev/null +++ b/packages/core/acl/src/__tests__/acl-role.test.ts @@ -0,0 +1,579 @@ +/** + * 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 { ACL } from '..'; +describe('multiple roles merge', () => { + let acl: ACL; + beforeEach(() => { + acl = new ACL(); + }); + describe('filter merge', () => { + test('should allow all(params:{}) when filter1 = undefined, filter2 is not exists', () => { + acl.setAvailableAction('edit', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + filter: undefined, + }, + }, + }); + acl.define({ + role: 'role2', + actions: {}, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: {}, + }); + }); + test('should allow all(params:{}) when filter1 = undefined, filter2 = {}', () => { + acl.setAvailableAction('edit', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + filter: undefined, + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': { + filter: {}, + }, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: {}, + }); + }); + test('should allow all(params={}) when filter1 = {}, filter2 = {}', () => { + acl.setAvailableAction('edit', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + filter: {}, + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': { + filter: {}, + }, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: {}, + }); + }); + test('should union filter(params.filter={$or:[{id:1}, {id:2}]}) when filter1 = {id: 1}, filter2 = {id: 2}', () => { + acl.setAvailableAction('edit', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + filter: { id: 1 }, + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': { + filter: { id: 2 }, + }, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: { + filter: { + $or: expect.arrayContaining([{ id: 1 }, { id: 2 }]), + }, + }, + }); + }); + + test('should union filter(filter={$or:[{id:1}, {name: zhangsan}]}) when filter1 = {id: 1}, filter2 = {name: zhangsan}', () => { + acl.setAvailableAction('edit', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + filter: { id: 1 }, + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': { + filter: { name: 'zhangsan' }, + }, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: { + filter: { + $or: expect.arrayContaining([{ id: 1 }, { name: 'zhangsan' }]), + }, + }, + }); + }); + + test('should union filter(filter={$or:[{id:1}, {name: zhangsan}]}) when filter1 = {id: 1}, filter2 = { $or: [{name: zhangsan}]', () => { + acl.setAvailableAction('edit', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + filter: { id: 1 }, + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': { + filter: { name: 'zhangsan' }, + }, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: { + filter: { + $or: expect.arrayContaining([{ id: 1 }, { name: 'zhangsan' }]), + }, + }, + }); + }); + }); + + describe('feilds merge', () => { + test('should allow all(params={}) when fields1 = undefined, fields2 is not exists', () => { + acl.setAvailableAction('edit', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + fields: [], + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': {}, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: {}, + }); + }); + test('should allow all(params={}) when fields1 = undefined, fields2 is not exists', () => { + acl.setAvailableAction('edit', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + fields: undefined, + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': {}, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: {}, + }); + }); + test('should allow all(params={}) when fields1 = [], fields2 =[]', () => { + acl.setAvailableAction('edit', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + fields: [], + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': { + fields: [], + }, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: {}, + }); + }); + test('should union fields(params={ fields: [a,b]}) when fields1 = [a], fields2 =[b]', () => { + acl.setAvailableAction('edit', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + fields: ['a'], + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': { + fields: ['b'], + }, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: { + fields: expect.arrayContaining(['a', 'b']), + }, + }); + }); + test('should union no repeat fields(params={ fields: [a,b,c]}) when fields1 = [a,b], fields2 =[b,c]', () => { + acl.setAvailableAction('edit', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + fields: ['a', 'b'], + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': { + fields: ['b', 'c'], + }, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: { + fields: expect.arrayContaining(['a', 'b', 'c']), + }, + }); + expect(canResult.params.fields.length).toStrictEqual(3); + }); + }); + + describe('whitelist', () => { + test('should union whitelist(params={ fields: [a,b,c]}) when fields1 = [a,b], fields2 =[c]', () => { + acl.setAvailableAction('update'); + acl.define({ + role: 'role1', + actions: { + 'posts:update': { + whitelist: ['a', 'b'], + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:update': { + whitelist: ['c'], + }, + }, + }); + const canResult = acl.can({ resource: 'posts', action: 'update', roles: ['role1', 'role2'] }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'update', + params: { + whitelist: expect.arrayContaining(['a', 'b', 'c']), + }, + }); + }); + }); + + describe('appends', () => { + test('should union appends(params={ appends: [a,b,c]}) when appends = [a,b], appends =[c]', () => { + acl.setAvailableAction('update'); + acl.define({ + role: 'role1', + actions: { + 'posts:update': { + appends: ['a', 'b'], + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:update': { + appends: ['c'], + }, + }, + }); + const canResult = acl.can({ resource: 'posts', action: 'update', roles: ['role1', 'role2'] }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'update', + params: { + appends: expect.arrayContaining(['a', 'b', 'c']), + }, + }); + }); + test('should union appends(params={ appends: [a,b]}) when appends = [a,b], appends =[]', () => { + acl.setAvailableAction('update'); + acl.define({ + role: 'role1', + actions: { + 'posts:update': { + appends: ['a', 'b'], + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:update': { + appends: [], + }, + }, + }); + const canResult = acl.can({ resource: 'posts', action: 'update', roles: ['role1', 'role2'] }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'update', + params: { + appends: expect.arrayContaining(['a', 'b']), + }, + }); + }); + }); + + describe('filter & fields merge', () => { + test('should allow all(params={}) when actions1 = {filter: {}}, actions2 = {fields: []}', () => { + acl.setAvailableAction('view', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + filter: {}, + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': { + fields: [], + }, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: {}, + }); + }); + test('should allow all(params={}) when actions1 = {filter: {}}, actions2 = {fields: [a]}', () => { + acl.setAvailableAction('view', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + filter: {}, + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': { + fields: ['a'], + }, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: {}, + }); + }); + test('should allow all(params={}) when actions1 = {filter: {a:1}}, actions2 = {fields: []}', () => { + acl.setAvailableAction('view', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + filter: { a: 1 }, + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': { + fields: [], + }, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: {}, + }); + }); + + test('should allow all(params={}) when actions1 = {filter: {a:1}}, actions2 = {fields: [a]}', () => { + acl.setAvailableAction('view', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + filter: { a: 1 }, + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': { + fields: ['a'], + }, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: {}, + }); + }); + + test('should union filter&fields(params={ filter:{ $or:[{a:1},{a:2}]}, fields:[a,b]}) when actions1={filter:{a:1}, fields:[a]}, actions2={filter: {a:1}},fields:[b]}', () => { + acl.setAvailableAction('view', { + type: 'old-data', + }); + acl.define({ + role: 'role1', + actions: { + 'posts:view': { + filter: { a: 1 }, + fields: ['a'], + }, + }, + }); + acl.define({ + role: 'role2', + actions: { + 'posts:view': { + filter: { a: 2 }, + fields: ['b'], + }, + }, + }); + const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' }); + expect(canResult).toStrictEqual({ + role: 'role1', + resource: 'posts', + action: 'view', + params: expect.objectContaining({ + filter: { $or: expect.arrayContaining([{ a: 1 }, { a: 2 }]) }, + fields: expect.arrayContaining(['a', 'b']), + }), + }); + }); + }); +}); diff --git a/packages/core/acl/src/acl-role.ts b/packages/core/acl/src/acl-role.ts index e2f4b53a03..a430a2033a 100644 --- a/packages/core/acl/src/acl-role.ts +++ b/packages/core/acl/src/acl-role.ts @@ -7,11 +7,11 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { default as _, default as lodash } from 'lodash'; +import minimatch from 'minimatch'; import { ACL, DefineOptions } from './acl'; import { ACLAvailableStrategy, AvailableStrategyOptions } from './acl-available-strategy'; import { ACLResource } from './acl-resource'; -import lodash from 'lodash'; -import minimatch from 'minimatch'; export interface RoleActionParams { fields?: string[]; @@ -185,12 +185,12 @@ export class ACLRole { } } - return { + return _.cloneDeep({ role: this.name, strategy: this.strategy, actions, snippets: Array.from(this.snippets), - }; + }); } protected getResourceActionFromPath(path: string) { diff --git a/packages/core/acl/src/acl.ts b/packages/core/acl/src/acl.ts index 00e86e5ceb..1c2ef37895 100644 --- a/packages/core/acl/src/acl.ts +++ b/packages/core/acl/src/acl.ts @@ -19,6 +19,7 @@ import { AllowManager, ConditionFunc } from './allow-manager'; import FixedParamsManager, { Merger } from './fixed-params-manager'; import SnippetManager, { SnippetOptions } from './snippet-manager'; import { NoPermissionError } from './errors/no-permission-error'; +import { mergeAclActionParams, removeEmptyParams } from './utils'; interface CanResult { role: string; @@ -54,11 +55,12 @@ export interface ListenerContext { type Listener = (ctx: ListenerContext) => void; interface CanArgs { - role: string; + role?: string; resource: string; action: string; rawResourceName?: string; ctx?: any; + roles?: string[]; } export class ACL extends EventEmitter { @@ -169,6 +171,10 @@ export class ACL extends EventEmitter { return this.roles.get(name); } + getRoles(names: string[]): ACLRole[] { + return names.map((name) => this.getRole(name)).filter((x) => Boolean(x)); + } + removeRole(name: string) { return this.roles.delete(name); } @@ -202,6 +208,36 @@ export class ACL extends EventEmitter { } can(options: CanArgs): CanResult | null { + if (options.role) { + return lodash.cloneDeep(this.getCanByRole(options)); + } + if (options.roles?.length) { + return lodash.cloneDeep(this.getCanByRoles(options)); + } + + return null; + } + + private getCanByRoles(options: CanArgs) { + let canResult: CanResult | null = null; + + for (const role of options.roles) { + const result = this.getCanByRole({ + role, + ...options, + }); + if (!canResult) { + canResult = result; + canResult && removeEmptyParams(canResult.params); + } else if (canResult && result) { + canResult.params = mergeAclActionParams(canResult.params, result.params); + } + } + + return canResult; + } + + private getCanByRole(options: CanArgs) { const { role, resource, action, rawResourceName } = options; const aclRole = this.roles.get(role); @@ -351,9 +387,12 @@ export class ACL extends EventEmitter { } ctx.can = (options: Omit) => { - const canResult = acl.can({ role: roleName, ...options }); - - return canResult; + const roles = ctx.state.currentRoles || [roleName]; + const can = acl.can({ roles, ...options }); + if (!can) { + return null; + } + return can; }; ctx.permission = { @@ -370,7 +409,7 @@ export class ACL extends EventEmitter { * @internal */ async getActionParams(ctx) { - const roleName = ctx.state.currentRole || 'anonymous'; + const roleNames = ctx.state.currentRoles?.length ? ctx.state.currentRoles : 'anonymous'; const { resourceName: rawResourceName, actionName } = ctx.action; let resourceName = rawResourceName; @@ -386,11 +425,11 @@ export class ACL extends EventEmitter { } ctx.can = (options: Omit) => { - const can = this.can({ role: roleName, ...options }); - if (!can) { - return null; + const can = this.can({ roles: roleNames, ...options }); + if (can) { + return lodash.cloneDeep(can); } - return lodash.cloneDeep(can); + return null; }; ctx.permission = { @@ -421,6 +460,23 @@ export class ACL extends EventEmitter { } } + // 检查 $or 条件中的 createdById + if (params?.filter?.$or?.length) { + const checkCreatedById = (items) => { + return items.some( + (x) => + 'createdById' in x || x.$or?.some((y) => 'createdById' in y) || x.$and?.some((y) => 'createdById' in y), + ); + }; + + if (checkCreatedById(params.filter.$or)) { + const collection = ctx.db.getCollection(resourceName); + if (!collection || !collection.getField('createdById')) { + throw new NoPermissionError('createdById field not found'); + } + } + } + return params; } diff --git a/packages/core/acl/src/index.ts b/packages/core/acl/src/index.ts index 269a8f6678..7def1f2dca 100644 --- a/packages/core/acl/src/index.ts +++ b/packages/core/acl/src/index.ts @@ -14,3 +14,4 @@ export * from './acl-resource'; export * from './acl-role'; export * from './skip-middleware'; export * from './errors'; +export * from './utils'; diff --git a/packages/core/acl/src/utils/acl-role.ts b/packages/core/acl/src/utils/acl-role.ts new file mode 100644 index 0000000000..406ce36dec --- /dev/null +++ b/packages/core/acl/src/utils/acl-role.ts @@ -0,0 +1,213 @@ +/** + * 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 { assign } from '@nocobase/utils'; +import _ from 'lodash'; +import { ACLRole } from '../acl-role'; + +export function mergeRole(roles: ACLRole[]) { + const result: Record = { + roles: [], + strategy: {}, + actions: null, + snippets: [], + resources: null, + }; + const allSnippets: string[][] = []; + for (const role of roles) { + const jsonRole = role.toJSON(); + result.roles = mergeRoleNames(result.roles, jsonRole.role); + result.strategy = mergeRoleStrategy(result.strategy, jsonRole.strategy); + result.actions = mergeRoleActions(result.actions, jsonRole.actions); + result.resources = mergeRoleResources(result.resources, [...role.resources.keys()]); + if (_.isArray(jsonRole.snippets)) { + allSnippets.push(jsonRole.snippets); + } + } + result.snippets = mergeRoleSnippets(allSnippets); + return result; +} + +function mergeRoleNames(sourceRoleNames, newRoleName) { + return newRoleName ? sourceRoleNames.concat(newRoleName) : sourceRoleNames; +} + +function mergeRoleStrategy(sourceStrategy, newStrategy) { + if (!newStrategy) { + return sourceStrategy; + } + if (_.isArray(newStrategy.actions)) { + if (!sourceStrategy.actions) { + sourceStrategy.actions = newStrategy.actions; + } else { + const actions = sourceStrategy.actions.concat(newStrategy.actions); + return { + ...sourceStrategy, + actions: [...new Set(actions)], + }; + } + } + return sourceStrategy; +} + +function mergeRoleActions(sourceActions, newActions) { + if (_.isEmpty(sourceActions)) return newActions; + if (_.isEmpty(newActions)) return sourceActions; + + const result = {}; + [...new Set(Reflect.ownKeys(sourceActions).concat(Reflect.ownKeys(newActions)))].forEach((key) => { + if (_.has(sourceActions, key) && _.has(newActions, key)) { + result[key] = mergeAclActionParams(sourceActions[key], newActions[key]); + return; + } + result[key] = _.has(sourceActions, key) ? sourceActions[key] : newActions[key]; + }); + + return result; +} + +function mergeRoleSnippets(allRoleSnippets: string[][]): string[] { + if (!allRoleSnippets.length) { + return []; + } + + const allSnippets = allRoleSnippets.flat(); + const isExclusion = (value) => value.startsWith('!'); + const includes = new Set(allSnippets.filter((x) => !isExclusion(x))); + const excludes = new Set(allSnippets.filter(isExclusion)); + + // 统计 xxx.* 在多少个角色中存在 + const domainRoleMap = new Map>(); + allRoleSnippets.forEach((roleSnippets, i) => { + roleSnippets + .filter((x) => x.endsWith('.*') && !isExclusion(x)) + .forEach((include) => { + const domain = include.slice(0, -1); + if (!domainRoleMap.has(domain)) { + domainRoleMap.set(domain, new Set()); + } + domainRoleMap.get(domain).add(i); + }); + }); + + // 处理黑名单交集(只有所有角色都有 `!xxx` 才保留) + const excludesSet = new Set(); + for (const snippet of excludes) { + if (allRoleSnippets.every((x) => x.includes(snippet))) { + excludesSet.add(snippet); + } + } + + for (const [domain, indexes] of domainRoleMap.entries()) { + const fullDomain = `${domain}.*`; + + // xxx.* 存在时,覆盖 !xxx.* + if (includes.has(fullDomain)) { + excludesSet.delete(`!${fullDomain}`); + } + + // 计算 !xxx.yyy,当所有 xxx.* 角色都包含 !xxx.yyy 时才保留 + for (const roleIndex of indexes) { + for (const exclude of allRoleSnippets[roleIndex]) { + if (exclude.startsWith(`!${domain}`) && exclude !== `!${fullDomain}`) { + if ([...indexes].every((i) => allRoleSnippets[i].includes(exclude))) { + excludesSet.add(exclude); + } + } + } + } + } + + // 确保 !xxx.yyy 只有在 xxx.* 存在时才有效,同时解决 [xxx] 和 [!xxx] 冲突 + if (includes.size > 0) { + for (const x of [...excludesSet]) { + const exactMatch = x.slice(1); + const segments = exactMatch.split('.'); + if (segments.length > 1 && segments[1] !== '*') { + const parentDomain = segments[0] + '.*'; + if (!includes.has(parentDomain)) { + excludesSet.delete(x); + } + } + } + } + + return [...includes, ...excludesSet]; +} + +function mergeRoleResources(sourceResources, newResources) { + if (sourceResources === null) { + return newResources; + } + + return [...new Set(sourceResources.concat(newResources))]; +} + +export function mergeAclActionParams(sourceParams, targetParams) { + if (_.isEmpty(sourceParams) || _.isEmpty(targetParams)) { + return {}; + } + + // source 和 target 其中之一没有 fields 字段时, 最终希望没有此字段 + removeUnmatchedParams(sourceParams, targetParams, ['fields', 'whitelist', 'appends']); + + const andMerge = (x, y) => { + if (_.isEmpty(x) || _.isEmpty(y)) { + return []; + } + return _.uniq(x.concat(y)).filter(Boolean); + }; + + const mergedParams = assign(targetParams, sourceParams, { + own: (x, y) => x || y, + filter: (x, y) => { + if (_.isEmpty(x) || _.isEmpty(y)) { + return {}; + } + const xHasOr = _.has(x, '$or'), + yHasOr = _.has(y, '$or'); + let $or = [x, y]; + if (xHasOr && !yHasOr) { + $or = [...x.$or, y]; + } else if (!xHasOr && yHasOr) { + $or = [x, ...y.$or]; + } else if (xHasOr && yHasOr) { + $or = [...x.$or, ...y.$or]; + } + + return { $or: _.uniqWith($or, _.isEqual) }; + }, + fields: andMerge, + whitelist: andMerge, + appends: 'union', + }); + removeEmptyParams(mergedParams); + return mergedParams; +} + +export function removeEmptyParams(params) { + if (!_.isObject(params)) { + return; + } + Object.keys(params).forEach((key) => { + if (_.isEmpty(params[key])) { + delete params[key]; + } + }); +} + +function removeUnmatchedParams(source, target, keys: string[]) { + for (const key of keys) { + if (_.has(source, key) && !_.has(target, key)) { + delete source[key]; + } + if (!_.has(source, key) && _.has(target, key)) { + delete target[key]; + } + } +} diff --git a/packages/core/acl/src/utils/index.ts b/packages/core/acl/src/utils/index.ts new file mode 100644 index 0000000000..48071b231d --- /dev/null +++ b/packages/core/acl/src/utils/index.ts @@ -0,0 +1,10 @@ +/** + * 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. + */ + +export * from './acl-role'; diff --git a/packages/core/actions/package.json b/packages/core/actions/package.json index d1fc8fc2cd..715901d72f 100644 --- a/packages/core/actions/package.json +++ b/packages/core/actions/package.json @@ -1,14 +1,14 @@ { "name": "@nocobase/actions", - "version": "1.6.0-alpha.29", + "version": "1.7.0-alpha.3", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/cache": "1.6.0-alpha.29", - "@nocobase/database": "1.6.0-alpha.29", - "@nocobase/resourcer": "1.6.0-alpha.29" + "@nocobase/cache": "1.7.0-alpha.3", + "@nocobase/database": "1.7.0-alpha.3", + "@nocobase/resourcer": "1.7.0-alpha.3" }, "repository": { "type": "git", diff --git a/packages/core/app/client/.umirc.ts b/packages/core/app/client/.umirc.ts index 02ed3ea903..a5aefe1e05 100644 --- a/packages/core/app/client/.umirc.ts +++ b/packages/core/app/client/.umirc.ts @@ -17,7 +17,7 @@ export default defineConfig({ title: 'Loading...', devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false, favicons: [`${appPublicPath}favicon_no_exist.ico`], // 设置一个不存在的 favicon,防止显示 Umi 默认的 favicon - metas: [{ name: 'viewport', content: 'initial-scale=0.1' }], + metas: [{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' }], links: [{ rel: 'stylesheet', href: `${appPublicPath}global.css` }], headScripts: [ { diff --git a/packages/core/app/package.json b/packages/core/app/package.json index 77d15bdd0b..2522e6585f 100644 --- a/packages/core/app/package.json +++ b/packages/core/app/package.json @@ -1,17 +1,17 @@ { "name": "@nocobase/app", - "version": "1.6.0-alpha.29", + "version": "1.7.0-alpha.3", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/database": "1.6.0-alpha.29", - "@nocobase/preset-nocobase": "1.6.0-alpha.29", - "@nocobase/server": "1.6.0-alpha.29" + "@nocobase/database": "1.7.0-alpha.3", + "@nocobase/preset-nocobase": "1.7.0-alpha.3", + "@nocobase/server": "1.7.0-alpha.3" }, "devDependencies": { - "@nocobase/client": "1.6.0-alpha.29" + "@nocobase/client": "1.7.0-alpha.3" }, "repository": { "type": "git", diff --git a/packages/core/auth/package.json b/packages/core/auth/package.json index 67744172f5..e439e1c14b 100644 --- a/packages/core/auth/package.json +++ b/packages/core/auth/package.json @@ -1,16 +1,16 @@ { "name": "@nocobase/auth", - "version": "1.6.0-alpha.29", + "version": "1.7.0-alpha.3", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/actions": "1.6.0-alpha.29", - "@nocobase/cache": "1.6.0-alpha.29", - "@nocobase/database": "1.6.0-alpha.29", - "@nocobase/resourcer": "1.6.0-alpha.29", - "@nocobase/utils": "1.6.0-alpha.29", + "@nocobase/actions": "1.7.0-alpha.3", + "@nocobase/cache": "1.7.0-alpha.3", + "@nocobase/database": "1.7.0-alpha.3", + "@nocobase/resourcer": "1.7.0-alpha.3", + "@nocobase/utils": "1.7.0-alpha.3", "@types/jsonwebtoken": "^8.5.8", "jsonwebtoken": "^8.5.1" }, diff --git a/packages/core/auth/src/__tests__/base-auth.test.ts b/packages/core/auth/src/__tests__/base-auth.test.ts index 82df98f382..f2e65c53e3 100644 --- a/packages/core/auth/src/__tests__/base-auth.test.ts +++ b/packages/core/auth/src/__tests__/base-auth.test.ts @@ -10,7 +10,6 @@ import { vi } from 'vitest'; import { BaseAuth } from '../base/auth'; import { AuthErrorCode } from '../auth'; -import jwt from 'jsonwebtoken'; describe('base-auth', () => { it('should validate username', () => { diff --git a/packages/core/auth/src/__tests__/middleware.test.ts b/packages/core/auth/src/__tests__/middleware.test.ts index 214b11aba2..440b14c6c0 100644 --- a/packages/core/auth/src/__tests__/middleware.test.ts +++ b/packages/core/auth/src/__tests__/middleware.test.ts @@ -22,7 +22,7 @@ describe('middleware', () => { app = await createMockServer({ registerActions: true, acl: true, - plugins: ['users', 'auth', 'acl', 'field-sort', 'data-source-manager', 'error-handler'], + plugins: ['users', 'auth', 'acl', 'field-sort', 'data-source-manager', 'error-handler', 'system-settings'], }); // app.plugin(ApiKeysPlugin); @@ -85,4 +85,13 @@ describe('middleware', () => { expect(res.body.errors.some((error) => error.code === AuthErrorCode.EMPTY_TOKEN)).toBe(true); }); }); + + describe('not exist user', async () => { + it('should throw 401 when user not exist', async () => { + const notExistUserAgent = await agent.login(1001); + const res = await notExistUserAgent.resource('auth').check(); + expect(res.status).toBe(401); + expect(res.body.errors.some((error) => error.code === AuthErrorCode.NOT_EXIST_USER)).toBe(true); + }); + }); }); diff --git a/packages/core/auth/src/base/auth.ts b/packages/core/auth/src/base/auth.ts index 31359de40d..e9a32af8f9 100644 --- a/packages/core/auth/src/base/auth.ts +++ b/packages/core/auth/src/base/auth.ts @@ -118,6 +118,13 @@ export class BaseAuth extends Auth { ) : null; + if (!user) { + this.ctx.throw(401, { + message: this.ctx.t('User not found. Please sign in again to continue.', { ns: localeNamespace }), + code: AuthErrorCode.NOT_EXIST_USER, + }); + } + if (roleName) { this.ctx.headers['x-role'] = roleName; } diff --git a/packages/core/build/package.json b/packages/core/build/package.json index dc35a84417..ea52b3545f 100644 --- a/packages/core/build/package.json +++ b/packages/core/build/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/build", - "version": "1.6.0-alpha.29", + "version": "1.7.0-alpha.3", "description": "Library build tool based on rollup.", "main": "lib/index.js", "types": "./lib/index.d.ts", diff --git a/packages/core/cache/package.json b/packages/core/cache/package.json index 65bd38850e..3fadc2bc73 100644 --- a/packages/core/cache/package.json +++ b/packages/core/cache/package.json @@ -1,12 +1,12 @@ { "name": "@nocobase/cache", - "version": "1.6.0-alpha.29", + "version": "1.7.0-alpha.3", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/lock-manager": "1.6.0-alpha.29", + "@nocobase/lock-manager": "1.7.0-alpha.3", "bloom-filters": "^3.0.1", "cache-manager": "^5.2.4", "cache-manager-redis-yet": "^4.1.2" diff --git a/packages/core/cli/nocobase.conf.tpl b/packages/core/cli/nocobase.conf.tpl index a40343cc02..141ca74282 100644 --- a/packages/core/cli/nocobase.conf.tpl +++ b/packages/core/cli/nocobase.conf.tpl @@ -17,7 +17,7 @@ server { server_name _; root {{cwd}}/node_modules/@nocobase/app/dist/client; index index.html; - client_max_body_size 1000M; + client_max_body_size 0; access_log /var/log/nginx/nocobase.log apm; gzip on; diff --git a/packages/core/cli/package.json b/packages/core/cli/package.json index 65cc06caee..b3b33ffe98 100644 --- a/packages/core/cli/package.json +++ b/packages/core/cli/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/cli", - "version": "1.6.0-alpha.29", + "version": "1.7.0-alpha.3", "description": "", "license": "AGPL-3.0", "main": "./src/index.js", @@ -8,7 +8,7 @@ "nocobase": "./bin/index.js" }, "dependencies": { - "@nocobase/app": "1.6.0-alpha.29", + "@nocobase/app": "1.7.0-alpha.3", "@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.0-alpha.29" + "@nocobase/devtools": "1.7.0-alpha.3" }, "repository": { "type": "git", diff --git a/packages/core/cli/templates/plugin/src/locale/nl-NL.json b/packages/core/cli/templates/plugin/src/locale/nl-NL.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/packages/core/cli/templates/plugin/src/locale/nl-NL.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/core/client/package.json b/packages/core/client/package.json index eca2704242..878391f222 100644 --- a/packages/core/client/package.json +++ b/packages/core/client/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/client", - "version": "1.6.0-alpha.29", + "version": "1.7.0-alpha.3", "license": "AGPL-3.0", "main": "lib/index.js", "module": "es/index.mjs", @@ -9,15 +9,15 @@ "@ahooksjs/use-url-state": "3.5.1", "@ant-design/cssinjs": "^1.11.1", "@ant-design/icons": "^5.6.1", - "@ant-design/pro-layout": "^7.16.11", + "@ant-design/pro-layout": "^7.22.1", "@antv/g2plot": "^2.4.18", - "@budibase/handlebars-helpers": "^0.14.0", + "@budibase/handlebars-helpers": "0.14.0", "@ctrl/tinycolor": "^3.6.0", "@dnd-kit/core": "^5.0.1", "@dnd-kit/modifiers": "^6.0.0", "@dnd-kit/sortable": "^6.0.0", "@emotion/css": "^11.7.1", - "@formily/antd-v5": "1.1.9", + "@formily/antd-v5": "1.2.3", "@formily/core": "^2.2.27", "@formily/grid": "^2.2.27", "@formily/json-schema": "^2.2.27", @@ -27,12 +27,12 @@ "@formily/reactive-react": "^2.2.27", "@formily/shared": "^2.2.27", "@formily/validator": "^2.2.27", - "@nocobase/evaluators": "1.6.0-alpha.29", - "@nocobase/sdk": "1.6.0-alpha.29", - "@nocobase/utils": "1.6.0-alpha.29", "@nocobase/json-template-parser": "1.6.0-alpha.28", + "@nocobase/evaluators": "1.7.0-alpha.3", + "@nocobase/sdk": "1.7.0-alpha.3", + "@nocobase/utils": "1.7.0-alpha.3", "ahooks": "^3.7.2", - "antd": "5.12.8", + "antd": "5.24.2", "antd-style": "3.7.1", "axios": "^1.7.0", "bignumber.js": "^9.1.2", diff --git a/packages/core/client/src/acl/ACLProvider.tsx b/packages/core/client/src/acl/ACLProvider.tsx index 81519b5339..aa0626b439 100644 --- a/packages/core/client/src/acl/ACLProvider.tsx +++ b/packages/core/client/src/acl/ACLProvider.tsx @@ -102,6 +102,11 @@ export const useRoleRecheck = () => { }; }; +export const useCurrentRoleMode = () => { + const ctx = useContext(ACLContext); + return ctx?.data?.data?.roleMode; +}; + export const useACLContext = () => { return useContext(ACLContext); }; @@ -321,7 +326,7 @@ export const ACLActionProvider = (props) => { () => actionPath && parseAction(actionPath, { schema, recordPkValue }), [parseAction, actionPath, schema, recordPkValue], ); - if (uiButtonSchemasBlacklist.includes(currentUid)) { + if (uiButtonSchemasBlacklist?.includes(currentUid)) { return {props.children}; } if (!actionPath) { diff --git a/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx b/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx index ddb4bb16c5..f6c9704fb9 100644 --- a/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx +++ b/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Checkbox, message, Table } from 'antd'; +import { Checkbox, message, Table, TableProps } from 'antd'; import { omit } from 'lodash'; import React, { createContext, useContext, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -102,44 +102,46 @@ export const SettingsCenterConfigure = () => { expandable={{ defaultExpandAllRows: true, }} - columns={[ - { - dataIndex: 'title', - title: t('Plugin name'), - render: (value) => { - return compile(value); + columns={ + [ + { + dataIndex: 'title', + title: t('Plugin name'), + render: (value) => { + return compile(value); + }, }, - }, - { - dataIndex: 'accessible', - title: ( - <> - { - const values = allAclSnippets.map((v) => '!' + v); - if (!allChecked) { - await resource.remove({ - values, - }); - } else { - await resource.add({ - values, - }); - } - refresh(); - message.success(t('Saved successfully')); - }} - />{' '} - {t('Accessible')} - - ), - render: (_, record) => { - const checked = !snippets.includes('!' + record.aclSnippet); - return handleChange(checked, record)} />; + { + dataIndex: 'accessible', + title: ( + <> + { + const values = allAclSnippets.map((v) => '!' + v); + if (!allChecked) { + await resource.remove({ + values, + }); + } else { + await resource.add({ + values, + }); + } + refresh(); + message.success(t('Saved successfully')); + }} + />{' '} + {t('Accessible')} + + ), + render: (_, record) => { + const checked = !snippets.includes('!' + record.aclSnippet); + return handleChange(checked, record)} />; + }, }, - }, - ]} + ] as TableProps['columns'] + } dataSource={settings .filter((v) => { return v.isTopLevel !== false; diff --git a/packages/core/client/src/acl/Configuration/MenuConfigure.tsx b/packages/core/client/src/acl/Configuration/MenuConfigure.tsx index 9f31491b89..2ab40c0f4d 100644 --- a/packages/core/client/src/acl/Configuration/MenuConfigure.tsx +++ b/packages/core/client/src/acl/Configuration/MenuConfigure.tsx @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Checkbox, message, Table } from 'antd'; +import { Checkbox, message, Table, TableProps } from 'antd'; import { uniq } from 'lodash'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -121,40 +121,42 @@ export const MenuConfigure = () => { expandable={{ defaultExpandAllRows: true, }} - columns={[ - { - dataIndex: 'title', - title: t('Menu item title'), - }, - { - dataIndex: 'accessible', - title: ( - <> - { - if (allChecked) { - await resource.set({ - values: [], - }); - } else { - await resource.set({ - values: allUids, - }); - } - refresh(); - message.success(t('Saved successfully')); - }} - />{' '} - {t('Accessible')} - - ), - render: (_, schema) => { - const checked = uids.includes(schema.uid); - return handleChange(checked, schema)} />; + columns={ + [ + { + dataIndex: 'title', + title: t('Menu item title'), }, - }, - ]} + { + dataIndex: 'accessible', + title: ( + <> + { + if (allChecked) { + await resource.set({ + values: [], + }); + } else { + await resource.set({ + values: allUids, + }); + } + refresh(); + message.success(t('Saved successfully')); + }} + />{' '} + {t('Accessible')} + + ), + render: (_, schema: { uid: string }) => { + const checked = uids.includes(schema.uid); + return handleChange(checked, schema)} />; + }, + }, + ] as TableProps['columns'] + } dataSource={translateTitle(items)} /> ); diff --git a/packages/core/client/src/acl/Configuration/RolesResourcesActions.tsx b/packages/core/client/src/acl/Configuration/RolesResourcesActions.tsx index da896e68d5..3dab660bf7 100644 --- a/packages/core/client/src/acl/Configuration/RolesResourcesActions.tsx +++ b/packages/core/client/src/acl/Configuration/RolesResourcesActions.tsx @@ -10,7 +10,7 @@ import { FormItem, FormLayout } from '@formily/antd-v5'; import { ArrayField } from '@formily/core'; import { connect, useField, useForm } from '@formily/react'; -import { Checkbox, Table, Tag } from 'antd'; +import { Checkbox, Table, Tag, TableProps } from 'antd'; import { isEmpty } from 'lodash'; import React, { createContext } from 'react'; import { useTranslation } from 'react-i18next'; @@ -105,48 +105,50 @@ export const RolesResourcesActions = connect((props) => { className={antTableCell} size={'small'} pagination={false} - columns={[ - { - dataIndex: 'displayName', - title: t('Action display name'), - render: (value) => compile(value), - }, - { - dataIndex: 'onNewRecord', - title: t('Action type'), - render: (onNewRecord) => - onNewRecord ? ( - {t('Action on new records')} - ) : ( - {t('Action on existing records')} - ), - }, - { - dataIndex: 'enabled', - title: t('Allow'), - render: (enabled, action) => ( - { - toggleAction(action.name); - }} - /> - ), - }, - { - dataIndex: 'scope', - title: t('Data scope'), - render: (value, action) => - !action.onNewRecord && ( - { - setScope(action.name, scope); + columns={ + [ + { + dataIndex: 'displayName', + title: t('Action display name'), + render: (value) => compile(value), + }, + { + dataIndex: 'onNewRecord', + title: t('Action type'), + render: (onNewRecord) => + onNewRecord ? ( + {t('Action on new records')} + ) : ( + {t('Action on existing records')} + ), + }, + { + dataIndex: 'enabled', + title: t('Allow'), + render: (enabled, action) => ( + { + toggleAction(action.name); }} /> ), - }, - ]} + }, + { + dataIndex: 'scope', + title: t('Data scope'), + render: (value, action) => + !action.onNewRecord && ( + { + setScope(action.name, scope); + }} + /> + ), + }, + ] as TableProps['columns'] + } dataSource={availableActions?.map((item) => { let enabled = false; let scope = null; @@ -169,60 +171,62 @@ export const RolesResourcesActions = connect((props) => { className={antTableCell} pagination={false} dataSource={fieldPermissions} - columns={[ - { - dataIndex: ['uiSchema', 'title'], - title: t('Field display name'), - render: (value) => compile(value), - }, - ...availableActionsWithFields.map((action) => { - const checked = allChecked?.[action.name]; - return { - dataIndex: action.name, - title: ( - <> + columns={ + [ + { + dataIndex: ['uiSchema', 'title'], + title: t('Field display name'), + render: (value) => compile(value), + }, + ...availableActionsWithFields.map((action) => { + const checked = allChecked?.[action.name]; + return { + dataIndex: action.name, + title: ( + <> + { + const item = actionMap[action.name] || { + name: action.name, + }; + if (checked) { + item.fields = []; + } else { + item.fields = collectionFields?.map?.((item) => item.name); + } + actionMap[action.name] = item; + onChange(Object.values(actionMap)); + }} + />{' '} + {compile(action.displayName)} + + ), + render: (checked, field) => ( { const item = actionMap[action.name] || { name: action.name, }; + const fields: string[] = item.fields || []; if (checked) { - item.fields = []; + const index = fields.indexOf(field.name); + fields.splice(index, 1); } else { - item.fields = collectionFields?.map?.((item) => item.name); + fields.push(field.name); } + item.fields = fields; actionMap[action.name] = item; onChange(Object.values(actionMap)); }} - />{' '} - {compile(action.displayName)} - - ), - render: (checked, field) => ( - { - const item = actionMap[action.name] || { - name: action.name, - }; - const fields: string[] = item.fields || []; - if (checked) { - const index = fields.indexOf(field.name); - fields.splice(index, 1); - } else { - fields.push(field.name); - } - item.fields = fields; - actionMap[action.name] = item; - onChange(Object.values(actionMap)); - }} - /> - ), - }; - }), - ]} + /> + ), + }; + }), + ] as TableProps['columns'] + } /> diff --git a/packages/core/client/src/acl/Configuration/StrategyActions.tsx b/packages/core/client/src/acl/Configuration/StrategyActions.tsx index df501db691..4efa5a8716 100644 --- a/packages/core/client/src/acl/Configuration/StrategyActions.tsx +++ b/packages/core/client/src/acl/Configuration/StrategyActions.tsx @@ -9,7 +9,7 @@ import { ArrayField } from '@formily/core'; import { connect, useField } from '@formily/react'; -import { Checkbox, Select, Table, Tag } from 'antd'; +import { Checkbox, Select, Table, Tag, TableProps } from 'antd'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useCompile } from '../..'; @@ -55,62 +55,64 @@ export const StrategyActions = connect((props) => { size={'small'} pagination={false} rowKey={'name'} - columns={[ - { - dataIndex: 'displayName', - title: t('Action display name'), - render: (value) => compile(value), - }, - { - dataIndex: 'onNewRecord', - title: t('Action type'), - render: (onNewRecord) => - onNewRecord ? ( - {t('Action on new records')} - ) : ( - {t('Action on existing records')} - ), - }, - { - dataIndex: 'enabled', - title: t('Allow'), - render: (enabled, action) => ( - { - if (enabled) { - delete scopes[action.name]; - } else { - scopes[action.name] = 'all'; - } - onChange(toFieldValue(scopes)); - }} - /> - ), - }, - { - dataIndex: 'scope', - title: t('Data scope'), - render: (scope, action) => - !action.onNewRecord && ( - { + scopes[action.name] = value; + onChange(toFieldValue(scopes)); + }} + /> + ), + }, + ] as TableProps['columns'] + } dataSource={availableActions?.map((item) => { let scope = 'all'; let enabled = false; diff --git a/packages/core/client/src/application/RouterManager.tsx b/packages/core/client/src/application/RouterManager.tsx index a8b08dafa0..cf16f2c9e4 100644 --- a/packages/core/client/src/application/RouterManager.tsx +++ b/packages/core/client/src/application/RouterManager.tsx @@ -9,6 +9,7 @@ import { get, set } from 'lodash'; import React, { ComponentType, createContext, useContext } from 'react'; +import { matchRoutes } from 'react-router'; import { BrowserRouterProps, createBrowserRouter, @@ -42,6 +43,7 @@ export type RouterOptions = (HashRouterOptions | BrowserRouterOptions | MemoryRo export type ComponentTypeAndString = ComponentType | string; export interface RouteType extends Omit { Component?: ComponentTypeAndString; + skipAuthCheck?: boolean; } export type RenderComponentType = (Component: ComponentTypeAndString, props?: any) => React.ReactNode; @@ -134,6 +136,18 @@ export class RouterManager { this.options.basename = basename; } + matchRoutes(pathname: string) { + const routes = Object.values(this.routes); + // @ts-ignore + return matchRoutes(routes, pathname, this.basename); + } + + isSkippedAuthCheckRoute(pathname: string) { + const matchedRoutes = this.matchRoutes(pathname); + return matchedRoutes.some((match) => { + return match?.route?.skipAuthCheck === true; + }); + } /** * @internal */ diff --git a/packages/core/client/src/application/__tests__/RouterManager.test.tsx b/packages/core/client/src/application/__tests__/RouterManager.test.tsx index 2ec1850a8e..0cf2419754 100644 --- a/packages/core/client/src/application/__tests__/RouterManager.test.tsx +++ b/packages/core/client/src/application/__tests__/RouterManager.test.tsx @@ -30,7 +30,7 @@ describe('Router', () => { let router: RouterManager; beforeEach(() => { - router = new RouterManager({ type: 'memory', initialEntries: ['/'] }, app); + router = new RouterManager({ type: 'memory', initialEntries: ['/'], basename: '/nocobase/apps/test1' }, app); }); it('basic', () => { @@ -132,6 +132,38 @@ describe('Router', () => { router.add('test', route); expect(router.getRoutesTree()).toEqual([{ path: '/', element: , children: undefined }]); }); + + it('add skipAuthCheck route', () => { + router.add('skip-auth-check', { path: '/skip-auth-check', Component: 'Hello', skipAuthCheck: true }); + router.add('not-skip-auth-check', { path: '/not-skip-auth-check', Component: 'Hello' }); + + const RouterComponent = router.getRouterComponent(); + const BaseLayout: FC = (props) => { + return
BaseLayout {props.children}
; + }; + render(); + router.navigate('/skip-auth-check'); + const state = router.state; + const { pathname, search } = state.location; + const isSkipedAuthCheck = router.isSkippedAuthCheckRoute(pathname); + expect(isSkipedAuthCheck).toBe(true); + }); + + it('add not skipAuthCheck route', () => { + router.add('skip-auth-check', { path: '/skip-auth-check', Component: 'Hello', skipAuthCheck: true }); + router.add('not-skip-auth-check', { path: '/not-skip-auth-check', Component: 'Hello' }); + + const RouterComponent = router.getRouterComponent(); + const BaseLayout: FC = (props) => { + return
BaseLayout {props.children}
; + }; + render(); + router.navigate('/not-skip-auth-check'); + const state = router.state; + const { pathname, search } = state.location; + const isSkipedAuthCheck = router.isSkippedAuthCheckRoute(pathname); + expect(isSkipedAuthCheck).toBe(false); + }); }); describe('remove', () => { diff --git a/packages/core/client/src/application/globalOperators.js b/packages/core/client/src/application/globalOperators.js index c3b16067e8..57b5ae7a11 100644 --- a/packages/core/client/src/application/globalOperators.js +++ b/packages/core/client/src/application/globalOperators.js @@ -124,7 +124,7 @@ export function getOperators() { return !a.includes(b); }, $anyOf: function (a, b) { - if (a.length === 0) { + if (a == null || a.length === 0) { return false; } if (Array.isArray(a) && Array.isArray(b) && a.some((element) => Array.isArray(element))) { @@ -347,7 +347,7 @@ export function getOperators() { /* This helper will defer to the JsonLogic spec as a tie-breaker when different language interpreters define different behavior for the truthiness of primitives. E.g., PHP considers empty arrays to be falsy, but Javascript considers them to be truthy. JsonLogic, as an ecosystem, needs one consistent answer. - + Spec and rationale here: http://jsonlogic.com/truthy */ jsonLogic.truthy = function (value) { @@ -397,7 +397,7 @@ export function getOperators() { if( 0 ){ 1 }else{ 2 }; if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 }; if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 }; - + The implementation is: For pairs of values (0,1 then 2,3 then 4,5 etc) If the first evaluates truthy, evaluate and return the second diff --git a/packages/core/client/src/application/hooks/index.ts b/packages/core/client/src/application/hooks/index.ts index 57fb89faff..3116f55c87 100644 --- a/packages/core/client/src/application/hooks/index.ts +++ b/packages/core/client/src/application/hooks/index.ts @@ -12,3 +12,4 @@ export * from './useAppSpin'; export * from './usePlugin'; export * from './useRouter'; export * from './useGlobalVariable'; +export * from './useAclSnippets'; diff --git a/packages/core/client/src/block-provider/TableBlockProvider.tsx b/packages/core/client/src/block-provider/TableBlockProvider.tsx index d1ae360457..7df1fecbfc 100644 --- a/packages/core/client/src/block-provider/TableBlockProvider.tsx +++ b/packages/core/client/src/block-provider/TableBlockProvider.tsx @@ -62,6 +62,7 @@ interface Props { children?: any; expandFlag?: boolean; dragSortBy?: string; + enableIndexÏColumn?: boolean; } const InternalTableBlockProvider = (props: Props) => { @@ -74,6 +75,7 @@ const InternalTableBlockProvider = (props: Props) => { expandFlag: propsExpandFlag = false, fieldNames, collection, + enableIndexÏColumn, } = props; const field: any = useField(); const { resource, service } = useBlockRequestContext(); @@ -131,6 +133,7 @@ const InternalTableBlockProvider = (props: Props) => { allIncludesChildren, setExpandFlag: setExpandFlagValue, heightProps, + enableIndexÏColumn, }), [ allIncludesChildren, @@ -146,6 +149,7 @@ const InternalTableBlockProvider = (props: Props) => { service, setExpandFlagValue, showIndex, + enableIndexÏColumn, ], ); diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 168374889f..1c7f80dd47 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -468,6 +468,10 @@ const useDoFilter = () => { block.defaultFilter, ]); + if (_.isEmpty(storedFilter[uid])) { + block.clearSelection?.(); + } + if (doNothingWhenFilterIsEmpty && _.isEmpty(storedFilter[uid])) { return; } @@ -1153,6 +1157,7 @@ export const useDetailsPaginationProps = () => { current: ctx.service?.data?.meta?.page || 1, pageSize: 1, showSizeChanger: false, + align: 'center', async onChange(page) { const params = ctx.service?.params?.[0]; ctx.service.run({ ...params, page }); @@ -1178,6 +1183,7 @@ export const useDetailsPaginationProps = () => { total: count, pageSize: 1, showSizeChanger: false, + align: 'center', async onChange(page) { const params = ctx.service?.params?.[0]; ctx.service.run({ ...params, page }); @@ -1349,6 +1355,7 @@ export const useAssociationFilterBlockProps = () => { [filterKey]: value, }; } else { + block.clearSelection?.(); if (block.dataLoadingMode === 'manual') { return block.clearData(); } @@ -1442,6 +1449,8 @@ async function doReset({ const target = targets.find((target) => target.uid === block.uid); if (!target) return; + block.clearSelection?.(); + if (block.dataLoadingMode === 'manual') { return block.clearData(); } diff --git a/packages/core/client/src/block-provider/hooks/useBlockHeightProps.tsx b/packages/core/client/src/block-provider/hooks/useBlockHeightProps.tsx index a412acba6c..66d8649ec0 100644 --- a/packages/core/client/src/block-provider/hooks/useBlockHeightProps.tsx +++ b/packages/core/client/src/block-provider/hooks/useBlockHeightProps.tsx @@ -11,7 +11,8 @@ import { useFieldSchema } from '@formily/react'; import { useMemo, useContext } from 'react'; import { useBlockTemplateContext } from '../../schema-templates/BlockTemplateProvider'; import { BlockItemCardContext } from '../../schema-component/antd/block-item/BlockItemCard'; -import { useCurrentRoute } from '../../route-switch'; +import { useAllAccessDesktopRoutes, findRouteBySchemaUid } from '../../route-switch/antd/admin-layout'; +import { useCurrentPageUid } from '../../application/CustomRouterContextProvider'; export const useBlockHeightProps = () => { const fieldSchema = useFieldSchema(); @@ -20,13 +21,19 @@ export const useBlockHeightProps = () => { const pageSchema = useMemo(() => getPageSchema(blockTemplateSchema || fieldSchema), []); const { disablePageHeader, enablePageTabs, hidePageTitle } = pageSchema?.['x-component-props'] || {}; const { titleHeight } = useContext(BlockItemCardContext) || ({} as any); - const currentRoute = useCurrentRoute(); + const { allAccessRoutes } = useAllAccessDesktopRoutes(); + const currentPageUid = useCurrentPageUid(); + + const currentRoute = useMemo( + () => findRouteBySchemaUid(currentPageUid, allAccessRoutes), + [currentPageUid, allAccessRoutes], + ); return { heightProps: { ...cardItemSchema?.['x-component-props'], title: cardItemSchema?.['x-component-props']?.title || cardItemSchema?.['x-component-props']?.description, disablePageHeader, - enablePageTabs: currentRoute.enableTabs || enablePageTabs, + enablePageTabs: currentRoute?.enableTabs || enablePageTabs, hidePageTitle, titleHeight: titleHeight, }, diff --git a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx index 0fb0ad2965..2e44491840 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx @@ -25,6 +25,7 @@ import { useCollectionManager_deprecated } from '../hooks'; import useDialect from '../hooks/useDialect'; import * as components from './components'; import { useFieldInterfaceOptions } from './interfaces'; +import { ItemType, MenuItemType } from 'antd/es/menu/interface'; const getSchema = (schema: CollectionFieldInterface, record: any, compile) => { if (!schema) { @@ -231,7 +232,7 @@ export const AddFieldAction = (props) => { }, [getTemplate, record]); const items = useMemo(() => { return getFieldOptions() - .map((option) => { + .map((option): ItemType & { title: string; children?: ItemType[] } => { if (option?.children?.length === 0) { return null; } diff --git a/packages/core/client/src/collection-manager/templates/components/PresetFields.tsx b/packages/core/client/src/collection-manager/templates/components/PresetFields.tsx index 675617818e..5f52ca897b 100644 --- a/packages/core/client/src/collection-manager/templates/components/PresetFields.tsx +++ b/packages/core/client/src/collection-manager/templates/components/PresetFields.tsx @@ -96,7 +96,7 @@ export const PresetFields = observer( rowSelection={{ type: 'checkbox', selectedRowKeys, - getCheckboxProps: (record) => ({ + getCheckboxProps: (record: { name: string }) => ({ name: record.name, disabled: props?.disabled || props?.presetFieldsDisabledIncludes?.includes?.(record.name), }), diff --git a/packages/core/client/src/css-variable/CSSVariableProvider.tsx b/packages/core/client/src/css-variable/CSSVariableProvider.tsx index dfb35f7d42..f7b56387c9 100644 --- a/packages/core/client/src/css-variable/CSSVariableProvider.tsx +++ b/packages/core/client/src/css-variable/CSSVariableProvider.tsx @@ -37,6 +37,7 @@ export const CSSVariableProvider = ({ children }) => { document.body.style.setProperty('--colorWarningBg', token.colorWarningBg); document.body.style.setProperty('--colorWarningBorder', token.colorWarningBorder); document.body.style.setProperty('--colorText', token.colorText); + document.body.style.setProperty('--colorTextHeaderMenu', token.colorTextHeaderMenu); document.body.style.setProperty('--colorPrimaryText', token.colorPrimaryText); document.body.style.setProperty('--colorPrimaryTextActive', token.colorPrimaryTextActive); document.body.style.setProperty('--colorPrimaryTextHover', token.colorPrimaryTextHover); @@ -48,6 +49,7 @@ export const CSSVariableProvider = ({ children }) => { document.body.style.setProperty('--colorBgSettingsHover', token.colorBgSettingsHover); document.body.style.setProperty('--colorTemplateBgSettingsHover', token.colorTemplateBgSettingsHover); document.body.style.setProperty('--colorBorderSettingsHover', token.colorBorderSettingsHover); + document.body.style.setProperty('--colorBgMenuItemSelected', token.colorBgHeaderMenuActive); // 设置登录页面的背景色 document.body.style.setProperty('background-color', token.colorBgContainer); @@ -76,6 +78,7 @@ export const CSSVariableProvider = ({ children }) => { token.marginXS, token.paddingContentVerticalSM, token.sizeXXL, + token.colorTextHeaderMenu, ]); return children; diff --git a/packages/core/client/src/data-source/collection-field/CollectionField.tsx b/packages/core/client/src/data-source/collection-field/CollectionField.tsx index 4bb5f6df4f..a2b2482ea7 100644 --- a/packages/core/client/src/data-source/collection-field/CollectionField.tsx +++ b/packages/core/client/src/data-source/collection-field/CollectionField.tsx @@ -83,6 +83,8 @@ const CollectionFieldInternalField_deprecated: React.FC = (props: Props) => { setRequired(field, fieldSchema, uiSchema); // @ts-ignore field.dataSource = uiSchema.enum; + field.data = field.data || {}; + field.data.dataSource = uiSchema?.enum; const originalProps = compile(uiSchema['x-component-props']) || {}; field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {}); }, [uiSchemaOrigin]); @@ -108,6 +110,8 @@ const CollectionFieldInternalField = (props) => { if (fieldSchema['x-read-pretty'] === true && !field.readPretty) { field.readPretty = true; } + field.data = field.data || {}; + field.data.dataSource = uiSchema?.enum; }, [field, fieldSchema]); if (!uiSchema) return null; diff --git a/packages/core/client/src/demo-utils/dataSourceMainCollections.json b/packages/core/client/src/demo-utils/dataSourceMainCollections.json index 848a38cdbc..c9c823d92a 100644 --- a/packages/core/client/src/demo-utils/dataSourceMainCollections.json +++ b/packages/core/client/src/demo-utils/dataSourceMainCollections.json @@ -241,36 +241,6 @@ } } }, - { - "key": "qe7b1rsct5h", - "name": "jobs", - "type": "belongsToMany", - "interface": null, - "description": null, - "collectionName": "users", - "parentKey": null, - "reverseKey": null, - "through": "users_jobs", - "foreignKey": "userId", - "sourceKey": "id", - "otherKey": "jobId", - "targetKey": "id", - "target": "jobs" - }, - { - "key": "vt0n1l1ruyz", - "name": "usersJobs", - "type": "hasMany", - "interface": null, - "description": null, - "collectionName": "users", - "parentKey": null, - "reverseKey": null, - "target": "users_jobs", - "foreignKey": "userId", - "sourceKey": "id", - "targetKey": "id" - }, { "key": "ekol7p60nry", "name": "sortName", diff --git a/packages/core/client/src/filter-provider/FilterProvider.tsx b/packages/core/client/src/filter-provider/FilterProvider.tsx index 6dab9105e0..6d1d9231ce 100644 --- a/packages/core/client/src/filter-provider/FilterProvider.tsx +++ b/packages/core/client/src/filter-provider/FilterProvider.tsx @@ -54,6 +54,8 @@ export interface DataBlock { clearFilter: (uid: string) => void; /** 将数据区块的数据置为空 */ clearData: () => void; + /** 清除表格的选中项 */ + clearSelection?: () => void; /** 数据区块表中所有的关系字段 */ associatedFields?: CollectionFieldOptions_deprecated[]; /** 数据区块表中所有的外键字段 */ @@ -165,16 +167,21 @@ export const DataBlockCollector = ({ clearData() { this.service.mutate(undefined); }, + clearSelection() { + if (field) { + field.data?.clearSelectedRowKeys?.(); + } + }, }); }, [ associatedFields, collection, dataLoadingMode, - field?.componentProps?.title, fieldSchema, params?.filter, recordDataBlocks, getDataBlockRequest, + field, ]); useEffect(() => { diff --git a/packages/core/client/src/filter-provider/utils.ts b/packages/core/client/src/filter-provider/utils.ts index 17dfe9df44..1b3c6920ba 100644 --- a/packages/core/client/src/filter-provider/utils.ts +++ b/packages/core/client/src/filter-provider/utils.ts @@ -215,7 +215,7 @@ export const useFilterAPI = () => { // 保留原有的 filter const storedFilter = block.service.params?.[1]?.filters || {}; - if (value !== undefined) { + if (value != null) { storedFilter[uid] = { $and: [ { @@ -226,7 +226,11 @@ export const useFilterAPI = () => { ], }; } else { + block.clearSelection?.(); delete storedFilter[uid]; + if (block.dataLoadingMode === 'manual') { + return block.clearData(); + } } const mergedFilter = mergeFilter([ diff --git a/packages/core/client/src/global-theme/defaultTheme.ts b/packages/core/client/src/global-theme/defaultTheme.ts index eb5de459d5..78c11135cf 100644 --- a/packages/core/client/src/global-theme/defaultTheme.ts +++ b/packages/core/client/src/global-theme/defaultTheme.ts @@ -29,7 +29,8 @@ const defaultTheme: ThemeConfig = { // 动画相关 motionUnit: 0.03, - motion: !process.env.__E2E__, + // ant design 升级到5.24.2后,Modal.confirm在E2E中如果关闭动画,会出现ant-modal-mask不销毁的问题 + // motion: !process.env.__E2E__, }, }; diff --git a/packages/core/client/src/global.less b/packages/core/client/src/global.less index 0a7b20ed1e..04061774d2 100644 --- a/packages/core/client/src/global.less +++ b/packages/core/client/src/global.less @@ -27,9 +27,21 @@ .rc-virtual-list-scrollbar-thumb { background: var(--colorBgScrollBar) !important; } + .rc-virtual-list-scrollbar-thumb:hover { background: var(--colorBgScrollBarHover) !important; } + .rc-virtual-list-scrollbar-thumb:active { background: var(--colorBgScrollBarActive) !important; } + + // Fix the style of the top collapsed menu button dropdown + .ant-menu-submenu-popup { + backdrop-filter: none; + } + + // Fix the style of the top collapsed menu button dropdown when clicking + .ant-menu-item.ant-menu-item-only-child.ant-pro-base-menu-horizontal-menu-item:active { + background-color: var(--colorBgMenuItemSelected) !important; + } diff --git a/packages/core/client/src/hoc/withTooltipComponent.tsx b/packages/core/client/src/hoc/withTooltipComponent.tsx new file mode 100644 index 0000000000..d057a033f7 --- /dev/null +++ b/packages/core/client/src/hoc/withTooltipComponent.tsx @@ -0,0 +1,37 @@ +/** + * 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 React from 'react'; +import { Tooltip } from 'antd'; +import { QuestionCircleOutlined } from '@ant-design/icons'; + +const titleWrapperStyle = { + display: 'flex', + alignItems: 'center', + gap: 4, +}; + +export const withTooltipComponent = (Component: React.FC) => { + return (props) => { + const { tooltip } = props; + + if (!tooltip) { + return ; + } + + return ( +
+ + + + +
+ ); + }; +}; diff --git a/packages/core/client/src/index.ts b/packages/core/client/src/index.ts index 8b33d7e223..2da90be022 100644 --- a/packages/core/client/src/index.ts +++ b/packages/core/client/src/index.ts @@ -71,16 +71,14 @@ export * from './modules/blocks/data-blocks/table'; export * from './modules/blocks/data-blocks/table-selector'; export * from './modules/blocks/index'; export * from './modules/blocks/useParentRecordCommon'; -export { getGroupMenuSchema } from './modules/menu/GroupItem'; -export { getLinkMenuSchema } from './modules/menu/LinkMenuItem'; -export { getPageMenuSchema } from './modules/menu/PageMenuItem'; +export { getPageMenuSchema, useInsertPageSchema } from './modules/menu/PageMenuItem'; export { OpenModeProvider, useOpenModeContext } from './modules/popup/OpenModeProvider'; export { PopupContextProvider } from './modules/popup/PopupContextProvider'; export { usePopupUtils } from './modules/popup/usePopupUtils'; export { VariablePopupRecordProvider } from './modules/variable/variablesProvider/VariablePopupRecordProvider'; -export { useCurrentPopupRecord } from './modules/variable/variablesProvider/VariablePopupRecordProvider'; export { showFileName } from './modules/fields/component/FileManager/fileManagerComponentFieldSettings'; +export { useCurrentPopupRecord } from './modules/variable/variablesProvider/VariablePopupRecordProvider'; export { languageCodes } from './locale'; diff --git a/packages/core/client/src/locale/de-DE.json b/packages/core/client/src/locale/de-DE.json new file mode 100644 index 0000000000..1f0df5805f --- /dev/null +++ b/packages/core/client/src/locale/de-DE.json @@ -0,0 +1,888 @@ +{ + "Display <1><0>10<1>20<2>50<3>100 items per page": "<1><0>10<1>20<2>50<3>100 Einträge pro Seite anzeigen", + "Meet <1><0>All<1>Any conditions in the group": "<1><0>Alle<1>Beliebige Bedingungen in der Gruppe erfüllen", + "Open in<1><0>Modal<1>Drawer<2>Window": "Öffnen in<1><0>Modal<1>Seitenleiste<2>Fenster", + "{{count}} filter items": "{{count}} Filterelemente", + "{{count}} more items": "{{count}} weitere Einträge", + "Total {{count}} items": "Insgesamt {{count}} Einträge", + "Today": "Heute", + "Yesterday": "Gestern", + "Tomorrow": "Morgen", + "Month": "Monat", + "Week": "Woche", + "This week": "Diese Woche", + "This month": "Dieser Monat", + "This year": "Dieses Jahr", + "Next year": "Nächstes Jahr", + "Last week": "Letzte Woche", + "Next week": "Nächste Woche", + "Last month": "Letzter Monat", + "Next month": "Nächster Monat", + "Last quarter": "Letztes Quartal", + "This quarter": "Dieses Quartal", + "Next quarter": "Nächstes Quartal", + "Last year": "Letztes Jahr", + "Last 7 days": "Letzte 7 Tage", + "Last 30 days": "Letzte 30 Tage", + "Last 90 days": "Letzte 90 Tage", + "Next 7 days": "Nächste 7 Tage", + "Next 30 days": "Nächste 30 Tage", + "Next 90 days": "Nächste 90 Tage", + "Work week": "Arbeitswoche", + "Day": "Tag", + "Agenda": "Agenda", + "Date": "Datum", + "Time": "Zeit", + "Event": "Ereignis", + "None": "Keine", + "Unconnected": "Nicht verbunden", + "System settings": "Systemeinstellungen", + "System title": "Systemtitel", + "Settings": "Einstellungen", + "Logo": "Logo", + "Add menu item": "Menüpunkt hinzufügen", + "Page": "Seite", + "Name": "Name", + "Icon": "Symbol", + "Group": "Gruppe", + "Link": "Link", + "Tab": "Tab", + "Save conditions": "Bedingungen speichern", + "Edit menu item": "Menüpunkt bearbeiten", + "Move to": "Verschieben nach", + "Insert left": "Links einfügen", + "Insert right": "Rechts einfügen", + "Insert inner": "Innen einfügen", + "Delete": "Löschen", + "Disassociate": "Trennen", + "Disassociate record": "Datensatz trennen", + "Are you sure you want to disassociate it?": "Sind Sie sicher, dass Sie die Verbindung trennen möchten?", + "UI editor": "UI-Editor", + "Collection": "Sammlung", + "Collection selector": "Sammlungsauswahl", + "Providing certain collections as options for users, typically used in polymorphic or inheritance scenarios": "Bestimmte Sammlungen als Optionen für Benutzer bereitstellen, typischerweise verwendet in polymorphen oder Vererbungsszenarien", + "Collections & Fields": "Sammlungen & Felder", + "All collections": "Alle Sammlungen", + "Add category": "Kategorie hinzufügen", + "Enable child collections": "Untersammlungen aktivieren", + "Allow adding records to the current collection": "Hinzufügen von Datensätzen zur aktuellen Sammlung erlauben", + "Delete category": "Kategorie löschen", + "Edit category": "Kategorie bearbeiten", + "Collection category": "Sammlungskategorie", + "Collection template": "Sammlungsvorlage", + "Sort": "Sortieren", + "Categories": "Kategorien", + "Visible": "Sichtbar", + "Read only": "Nur lesen", + "Easy reading": "Leicht lesbar", + "Hidden": "Versteckt", + "Hidden(reserved value)": "Versteckt (reservierter Wert)", + "Not required": "Nicht erforderlich", + "Value": "Wert", + "Disabled": "Deaktiviert", + "Enabled": "Aktiviert", + "Problematic": "Problematisch", + "Setting": "Einstellung", + "On": "Ein", + "Off": "Aus", + "Empty": "Leer", + "Linkage rule": "Verknüpfungsregel", + "Linkage rules": "Verknüpfungsregeln", + "Condition": "Bedingung", + "Properties": "Eigenschaften", + "Add linkage rule": "Verknüpfungsregel hinzufügen", + "Add property": "Eigenschaft hinzufügen", + "Category name": "Kategoriename", + "Roles & Permissions": "Rollen & Berechtigungen", + "Edit profile": "Profil bearbeiten", + "Change password": "Passwort ändern", + "Old password": "Altes Passwort", + "New password": "Neues Passwort", + "Switch role": "Rolle wechseln", + "Super admin": "Superadministrator", + "Language": "Sprache", + "Allow sign up": "Registrierung erlauben", + "Enable SMS authentication": "SMS-Authentifizierung aktivieren", + "Sign out": "Abmelden", + "Cancel": "Abbrechen", + "Submit": "Absenden", + "Close": "Schließen", + "Set the data scope": "Datenbereich festlegen", + "Set data loading mode": "Datenladungsmodus festlegen", + "Load all data when filter is empty": "Alle Daten laden, wenn der Filter leer ist", + "Do not load data when filter is empty": "Keine Daten laden, wenn der Filter leer ist", + "Data loading mode": "Datenladungsmodus", + "Data blocks": "Datenblöcke", + "Filter blocks": "Filterblöcke", + "Table": "Tabelle", + "Table OID(Inheritance)": "Tabellen-OID (Vererbung)", + "Form": "Formular", + "List": "Liste", + "Grid Card": "Rasterkarte", + "pixels": "Pixel", + "Screen size": "Bildschirmgröße", + "Display title": "Titel anzeigen", + "Set the count of columns displayed in a row": "Anzahl der Spalten in einer Zeile festlegen", + "Column": "Spalte", + "Phone device": "Mobiltelefon", + "Tablet device": "Tablet", + "Desktop device": "Desktop", + "Large screen device": "Großer Bildschirm", + "Collapse": "Einklappen", + "Select data source": "Datenquelle auswählen", + "Calendar": "Kalender", + "Delete events": "Ereignisse löschen", + "This event": "Dieses Ereignis", + "This and following events": "Dieses und folgende Ereignisse", + "All events": "Alle Ereignisse", + "Delete this event?": "Dieses Ereignis löschen?", + "Delete Event": "Ereignis löschen", + "Kanban": "Kanban", + "Gantt": "Gantt", + "Create gantt block": "Gantt-Block erstellen", + "Progress field": "Fortschrittsfeld", + "Time scale": "Zeitskala", + "Hour": "Stunde", + "Quarter of day": "Viertel des Tages", + "Half of day": "Halber Tag", + "Year": "Jahr", + "QuarterYear": "Jahresquartal", + "Select grouping field": "Gruppierungsfeld auswählen", + "Media": "Medien", + "Markdown": "Markdown", + "Wysiwyg": "Wysiwyg", + "Chart blocks": "Diagrammblöcke", + "Column chart": "Säulendiagramm", + "Bar chart": "Balkendiagramm", + "Line chart": "Liniendiagramm", + "Pie chart": "Kreisdiagramm", + "Area chart": "Flächendiagramm", + "Other chart": "Anderes Diagramm", + "Other blocks": "Andere Blöcke", + "In configuration": "In Konfiguration", + "Chart title": "Diagrammtitel", + "Chart type": "Diagrammtyp", + "Chart config": "Diagrammkonfiguration", + "Templates": "Vorlagen", + "Select template": "Vorlage auswählen", + "Action logs": "Aktionslogs", + "Create template": "Vorlage erstellen", + "Edit markdown": "Markdown bearbeiten", + "Add block": "Block hinzufügen", + "Add new": "Neu hinzufügen", + "Add record": "Datensatz hinzufügen", + "Add child": "Kind hinzufügen", + "Collapse all": "Alle einklappen", + "Expand all": "Alle ausklappen", + "Expand/Collapse": "Erweitern/Einklappen", + "Default collapse": "Standardmäßig eingeklappt", + "Tree table": "Baumtabelle", + "Custom field display name": "Benutzerdefinierter Feldanzeigename", + "Display fields": "Anzeigefelder der Sammlung", + "Edit record": "Datensatz bearbeiten", + "Delete menu item": "Menüpunkt löschen", + "Add page": "Seite hinzufügen", + "Add group": "Gruppe hinzufügen", + "Add link": "Link hinzufügen", + "Insert above": "Oben einfügen", + "Insert below": "Unten einfügen", + "Save": "Speichern", + "Delete block": "Block löschen", + "Are you sure you want to delete it?": "Sind Sie sicher, dass Sie es löschen möchten?", + "This is a demo text, **supports Markdown syntax**.": "Dies ist ein Beispieltext, **unterstützt Markdown-Syntax**.", + "Filter": "Filter", + "Connect data blocks": "Datenblöcke verbinden", + "Action type": "Aktionstyp", + "Actions": "Aktionen", + "Insert": "Einfügen", + "Insert if not exists": "Einfügen, wenn nicht vorhanden", + "Insert if not exists, or update": "Einfügen, wenn nicht vorhanden, sonst aktualisieren", + "Determine whether a record exists by the following fields": "Bestimmen Sie, ob ein Datensatz anhand der folgenden Felder existiert", + "Update": "Aktualisieren", + "Update record": "Datensatz aktualisieren", + "View": "Ansicht", + "View record": "Datensatz ansehen", + "Refresh": "Aktualisieren", + "Data changes": "Datenänderungen", + "Field name": "Feldname", + "Before change": "Vor der Änderung", + "After change": "Nach der Änderung", + "Delete record": "Datensatz löschen", + "Delete collection": "Sammlung löschen", + "Create collection": "Sammlung erstellen", + "Collection display name": "Anzeigename der Sammlung", + "Collection name": "Sammlungsname", + "Inherits": "Erbt von", + "Primary key, unique identifier, self growth": "Primärschlüssel, eindeutiger Bezeichner, automatische Erhöhung", + "Store the creation user of each record": "Speichert den Erstellungsbenutzer jedes Datensatzes", + "Store the last update user of each record": "Speichert den letzten Aktualisierungsbenutzer jedes Datensatzes", + "Store the creation time of each record": "Speichert die Erstellungszeit jedes Datensatzes", + "Store the last update time of each record": "Speichert die letzte Aktualisierungszeit jedes Datensatzes", + "More options": "Weitere Optionen", + "Records can be sorted": "Datensätze können sortiert werden", + "Calendar collection": "Kalendersammlung", + "General collection": "Allgemeine Sammlung", + "Connect to database view": "Mit Datenbankansicht verbinden", + "Sync from database": "Von Datenbank synchronisieren", + "Source collections": "Quellsammlungen", + "Field source": "Feldquelle", + "Preview": "Vorschau", + "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Zufällig generiert und kann geändert werden. Unterstützt Buchstaben, Zahlen und Unterstriche, muss mit einem Buchstaben beginnen.", + "Edit": "Bearbeiten", + "Edit collection": "Sammlung bearbeiten", + "Configure fields": "Felder konfigurieren", + "Configure columns": "Spalten konfigurieren", + "Edit field": "Feld bearbeiten", + "Override": "Überschreiben", + "Override field": "Feld überschreiben", + "Configure fields of {{title}}": "Felder von {{title}} konfigurieren", + "Association fields filter": "Filter für Verknüpfungsfelder", + "PK & FK fields": "PK & FK Felder", + "Association fields": "Verknüpfungsfelder", + "Choices fields": "Auswahlfelder", + "System fields": "Systemfelder", + "General fields": "Allgemeine Felder", + "Inherited fields": "Geerbte Felder", + "Parent collection fields": "Felder der übergeordneten Sammlung", + "Basic": "Grundlegend", + "Single line text": "Einzeiliger Text", + "Long text": "Langer Text", + "Phone": "Telefon", + "Email": "E-Mail", + "Number": "Zahl", + "Integer": "Ganzzahl", + "Percent": "Prozent", + "Password": "Passwort", + "Advanced type": "Erweitert", + "Formula": "Formel", + "Formula description": "Berechnet einen Wert in jedem Datensatz basierend auf anderen Feldern im selben Datensatz.", + "Choices": "Auswahlmöglichkeiten", + "Checkbox": "Kontrollkästchen", + "Single select": "Einzelauswahl", + "Multiple select": "Mehrfachauswahl", + "Radio group": "Radiogruppe", + "Checkbox group": "Kontrollkästchengruppe", + "China region": "China-Region", + "Date & Time": "Datum & Zeit", + "Datetime": "Datum/Zeit", + "Relation": "Beziehung", + "Link to": "Verknüpfen mit", + "Link to description": "Wird verwendet, um Sammlungsbeziehungen schnell zu erstellen und ist mit den meisten gängigen Szenarien kompatibel. Geeignet für Nicht-Entwickler. Als Feld dargestellt, ist es eine Dropdown-Auswahl zur Auswahl von Datensätzen aus der Zielsammlung. Nach der Erstellung werden gleichzeitig die zugehörigen Felder der aktuellen Sammlung in der Zielsammlung generiert.", + "Sub-table": "Untertabelle", + "Sub-details": "Unterdetails", + "Sub-form(Popover)": "Unterformular (Popover)", + "System info": "Systeminformationen", + "Created at": "Erstellt am", + "Last updated at": "Zuletzt aktualisiert am", + "Created by": "Erstellt von", + "Last updated by": "Zuletzt aktualisiert von", + "Add field": "Feld hinzufügen", + "Field display name": "Feldanzeigename", + "Field type": "Feldtyp", + "Field interface": "Feldschnittstelle", + "Date format": "Datumsformat", + "Year/Month/Day": "Jahr/Monat/Tag", + "Year-Month-Day": "Jahr-Monat-Tag", + "Day/Month/Year": "Tag/Monat/Jahr", + "Show time": "Zeit anzeigen", + "Time format": "Zeitformat", + "12 hour": "12 Stunden", + "24 hour": "24 Stunden", + "Relationship type": "Beziehungstyp", + "Inverse relationship type": "Inverse Beziehungstyp", + "Source collection": "Quellsammlung", + "Source key": "Quellschlüssel", + "Target collection": "Zielsammlung", + "Through collection": "Zwischensammlung", + "Target key": "Zielschlüssel", + "Foreign key": "Fremdschlüssel", + "One to one": "Eins zu Eins", + "One to many": "Eins zu Viele", + "Many to one": "Viele zu Eins", + "Many to many": "Viele zu Viele", + "Foreign key 1": "Fremdschlüssel 1", + "Foreign key 2": "Fremdschlüssel 2", + "One to one description": "Wird verwendet, um eine Eins-zu-Eins-Beziehung zu erstellen. Zum Beispiel hat ein Benutzer ein Profil.", + "One to many description": "Wird verwendet, um eine Eins-zu-Viele-Beziehung zu erstellen. Zum Beispiel hat ein Land viele Städte und eine Stadt kann nur in einem Land sein. Als Feld dargestellt, ist es eine Untertabelle, die die Datensätze der zugehörigen Sammlung anzeigt. Bei der Erstellung wird automatisch ein Viele-zu-Eins-Feld in der zugehörigen Sammlung generiert.", + "Many to one description": "Wird verwendet, um Viele-zu-Eins-Beziehungen zu erstellen. Zum Beispiel kann eine Stadt nur zu einem Land gehören und ein Land kann viele Städte haben. Als Feld dargestellt, ist es eine Dropdown-Auswahl zur Auswahl eines Datensatzes aus der zugehörigen Sammlung. Nach der Erstellung wird automatisch ein Eins-zu-Viele-Feld in der zugehörigen Sammlung generiert.", + "Many to many description": "Wird verwendet, um Viele-zu-Viele-Beziehungen zu erstellen. Zum Beispiel hat ein Schüler viele Lehrer und ein Lehrer hat viele Schüler. Als Feld dargestellt, ist es eine Dropdown-Auswahl zur Auswahl von Datensätzen aus der zugehörigen Sammlung.", + "Generated automatically if left blank": "Wird automatisch generiert, wenn leer gelassen", + "Display association fields": "Verknüpfungsfelder anzeigen", + "Display field title": "Feldtitel anzeigen", + "Field component": "Feldkomponente", + "Allow multiple": "Mehrere erlauben", + "Quick upload": "Schnelles Hochladen", + "Select file": "Datei auswählen", + "Subtable": "Untertabelle", + "Sub-form": "Unterformular", + "Field mode": "Feldmodus", + "Allow add new data": "Hinzufügen neuer Daten erlauben", + "Record picker": "Datensatzauswahl", + "Toggles the subfield mode": "Schaltet den Unterfeld-Modus um", + "Selector mode": "Auswahlmodus", + "Subtable mode": "Untertabellenmodus", + "Subform mode": "Unterformularmodus", + "Edit block title": "Blocktitel bearbeiten", + "Block title": "Blocktitel", + "Pattern": "Muster", + "Operator": "Operator", + "Editable": "Bearbeitbar", + "Readonly": "Schreibgeschützt", + "Easy-reading": "Leicht lesbar", + "Add filter": "Filter hinzufügen", + "Add filter group": "Filtergruppe hinzufügen", + "Comparision": "Vergleich", + "is": "ist", + "is not": "ist nicht", + "contains": "enthält", + "does not contain": "enthält nicht", + "starts with": "beginnt mit", + "not starts with": "beginnt nicht mit", + "ends with": "endet mit", + "not ends with": "endet nicht mit", + "is empty": "ist leer", + "is not empty": "ist nicht leer", + "Edit chart": "Diagramm bearbeiten", + "Add text": "Text hinzufügen", + "Filterable fields": "Filterbare Felder", + "Edit button": "Schaltfläche bearbeiten", + "Hide": "Ausblenden", + "Enable actions": "Aktionen aktivieren", + "Import": "Importieren", + "Export": "Exportieren", + "Customize": "Anpassen", + "Custom": "Benutzerdefiniert", + "Function": "Funktion", + "Popup form": "Popup-Formular", + "Flexible popup": "Flexibles Popup", + "Configure actions": "Aktionen konfigurieren", + "Display order number": "Bestellnummer anzeigen", + "Enable drag and drop sorting": "Drag & Drop-Sortierung aktivieren", + "Triggered when the row is clicked": "Wird ausgelöst, wenn auf die Zeile geklickt wird", + "Add tab": "Tab hinzufügen", + "Disable tabs": "Tabs deaktivieren", + "Details": "Details", + "Edit form": "Formular bearbeiten", + "Create form": "Formular erstellen", + "Form (Edit)": "Formular (Bearbeiten)", + "Form (Add new)": "Formular (Neu hinzufügen)", + "Edit tab": "Tab bearbeiten", + "Relationship blocks": "Beziehungsblöcke", + "Select record": "Datensatz auswählen", + "Display name": "Anzeigename", + "Select icon": "Symbol auswählen", + "Custom column name": "Benutzerdefinierter Spaltenname", + "Edit description": "Beschreibung bearbeiten", + "Required": "Erforderlich", + "Unique": "Eindeutig", + "Primary": "Primär", + "Auto increment": "Automatische Erhöhung", + "Label field": "Beschriftungsfeld", + "Default is the ID field": "Standard ist das ID-Feld", + "Set default sorting rules": "Standardsortierregeln festlegen", + "Set validation rules": "Validierungsregeln festlegen", + "Max length": "Maximale Länge", + "Min length": "Minimale Länge", + "Maximum": "Maximum", + "Minimum": "Minimum", + "Max length must greater than min length": "Maximale Länge muss größer als minimale Länge sein", + "Min length must less than max length": "Minimale Länge muss kleiner als maximale Länge sein", + "Maximum must greater than minimum": "Maximum muss größer als Minimum sein", + "Minimum must less than maximum": "Minimum muss kleiner als Maximum sein", + "Validation rule": "Validierungsregel", + "Add validation rule": "Validierungsregel hinzufügen", + "Format": "Format", + "Regular expression": "Regulärer Ausdruck", + "Error message": "Fehlermeldung", + "Length": "Länge", + "The field value cannot be greater than ": "Der Feldwert darf nicht größer sein als ", + "The field value cannot be less than ": "Der Feldwert darf nicht kleiner sein als ", + "The field value is not an integer number": "Der Feldwert ist keine ganze Zahl", + "Set default value": "Standardwert festlegen", + "Default value": "Standardwert", + "is before": "ist vor", + "is after": "ist nach", + "is on or after": "ist am oder nach", + "is on or before": "ist am oder vor", + "is between": "ist zwischen", + "Upload": "Hochladen", + "Select level": "Ebene auswählen", + "Province": "Provinz", + "City": "Stadt", + "Area": "Gebiet", + "Street": "Straße", + "Village": "Dorf", + "Must select to the last level": "Muss bis zur letzten Ebene ausgewählt werden", + "Move {{title}} to": "{{title}} verschieben nach", + "Target position": "Zielposition", + "After": "Nach", + "Before": "Vor", + "Add {{type}} before \"{{title}}\"": "{{type}} vor \"{{title}}\" hinzufügen", + "Add {{type}} after \"{{title}}\"": "{{type}} nach \"{{title}}\" hinzufügen", + "Add {{type}} in \"{{title}}\"": "{{type}} in \"{{title}}\" hinzufügen", + "Original name": "Ursprünglicher Name", + "Custom name": "Benutzerdefinierter Name", + "Custom Title": "Benutzerdefinierter Titel", + "Options": "Optionen", + "Option value": "Optionswert", + "Option label": "Optionsbezeichnung", + "Color": "Farbe", + "Background Color": "Hintergrundfarbe", + "Text Align": "Textausrichtung", + "Add option": "Option hinzufügen", + "Related collection": "Zugehörige Sammlung", + "Allow linking to multiple records": "Verknüpfung mit mehreren Datensätzen erlauben", + "Allow uploading multiple files": "Hochladen mehrerer Dateien erlauben", + "Configure calendar": "Kalender konfigurieren", + "Title field": "Titelfeld", + "Custom title": "Benutzerdefinierter Titel", + "Daily": "Täglich", + "Weekly": "Wöchentlich", + "Monthly": "Monatlich", + "Yearly": "Jährlich", + "Repeats": "Wiederholungen", + "Show lunar": "Mondkalender anzeigen", + "Start date field": "Startdatumsfeld", + "End date field": "Enddatumsfeld", + "Navigate": "Navigieren", + "Title": "Titel", + "Description": "Beschreibung", + "Select view": "Ansicht auswählen", + "Reset": "Zurücksetzen", + "Importable fields": "Importierbare Felder", + "Exportable fields": "Exportierbare Felder", + "Saved successfully": "Erfolgreich gespeichert", + "Nickname": "Spitzname", + "Sign in": "Anmelden", + "Sign in via account": "Über Konto anmelden", + "Sign in via phone": "Über Telefon anmelden", + "Create an account": "Konto erstellen", + "Sign up": "Registrieren", + "Confirm password": "Passwort bestätigen", + "Log in with an existing account": "Mit einem bestehenden Konto anmelden", + "Signed up successfully. It will jump to the login page.": "Registrierung erfolgreich. Sie werden zur Anmeldeseite weitergeleitet.", + "Password mismatch": "Passwörter stimmen nicht überein", + "Users": "Benutzer", + "Verification code": "Bestätigungscode", + "Send code": "Code senden", + "Retry after {{count}} seconds": "Wiederholen nach {{count}} Sekunden", + "Roles": "Rollen", + "Add role": "Rolle hinzufügen", + "Role name": "Rollenname", + "Configure": "Konfigurieren", + "Configure permissions": "Berechtigungen konfigurieren", + "Edit role": "Rolle bearbeiten", + "Action permissions": "Aktionsberechtigungen", + "Menu permissions": "Menüberechtigungen", + "Menu item name": "Menüpunktname", + "Allow access": "Zugriff erlauben", + "Action name": "Aktionsname", + "Allow action": "Aktion erlauben", + "Action scope": "Aktionsbereich", + "Operate on new data": "Mit neuen Daten arbeiten", + "Operate on existing data": "Mit vorhandenen Daten arbeiten", + "Yes": "Ja", + "No": "Nein", + "Red": "Rot", + "Magenta": "Magenta", + "Volcano": "Vulkan", + "Orange": "Orange", + "Gold": "Gold", + "Lime": "Limette", + "Green": "Grün", + "Cyan": "Cyan", + "Blue": "Blau", + "Geek blue": "Geek-Blau", + "Purple": "Lila", + "Default": "Standard", + "Add card": "Karte hinzufügen", + "edit title": "Titel bearbeiten", + "Turn pages": "Seiten umblättern", + "Others": "Andere", + "Other records": "Andere Datensätze", + "Save as template": "Als Vorlage speichern", + "Save as block template": "Als Blockvorlage speichern", + "Block templates": "Blockvorlagen", + "Block template": "Blockvorlage", + "Convert reference to duplicate": "Referenz in Duplikat umwandeln", + "Template name": "Vorlagenname", + "Block type": "Blocktyp", + "No blocks to connect": "Keine Blöcke zum Verbinden", + "Action column": "Aktionsspalte", + "Records per page": "Datensätze pro Seite", + "(Fields only)": "(Nur Felder)", + "Button title": "Schaltflächentitel", + "Button icon": "Schaltflächensymbol", + "Submitted successfully": "Erfolgreich übermittelt", + "Operation succeeded": "Operation erfolgreich", + "Operation failed": "Operation fehlgeschlagen", + "Open mode": "Öffnungsmodus", + "Popup size": "Popup-Größe", + "Small": "Klein", + "Middle": "Mittel", + "Large": "Groß", + "Size": "Größe", + "Oversized": "Übergroß", + "Auto": "Automatisch", + "Object Fit": "Objektanpassung", + "Cover": "Abdecken", + "Fill": "Füllen", + "Contain": "Enthalten", + "Scale Down": "Verkleinern", + "Menu item title": "Menüpunkttitel", + "Menu item icon": "Menüpunktsymbol", + "Target": "Ziel", + "Position": "Position", + "Insert before": "Davor einfügen", + "Insert after": "Danach einfügen", + "UI Editor": "UI-Editor", + "ASC": "Aufsteigend", + "DESC": "Absteigend", + "Add sort field": "Sortierfeld hinzufügen", + "ID": "ID", + "Identifier for program usage. Support letters, numbers and underscores, must start with an letter.": "Bezeichner für Programmnutzung. Unterstützt Buchstaben, Zahlen und Unterstriche, muss mit einem Buchstaben beginnen.", + "Drawer": "Seitenleiste", + "Dialog": "Dialog", + "Delete action": "Aktion löschen", + "Custom column title": "Benutzerdefinierter Spaltentitel", + "Column title": "Spaltentitel", + "Original title: ": "Ursprünglicher Titel: ", + "Delete table column": "Tabellenspalte löschen", + "Skip required validation": "Erforderliche Validierung überspringen", + "Form values": "Formularwerte", + "Fields values": "Feldwerte", + "The field has been deleted": "Das Feld wurde gelöscht", + "When submitting the following fields, the saved values are": "Beim Absenden der folgenden Felder sind die gespeicherten Werte", + "After successful submission": "Nach erfolgreicher Übermittlung", + "Then": "Dann", + "Stay on current page": "Auf aktueller Seite bleiben", + "Redirect to": "Weiterleiten zu", + "Save action": "Aktion speichern", + "Exists": "Existiert", + "Add condition": "Bedingung hinzufügen", + "Add condition group": "Bedingungsgruppe hinzufügen", + "exists": "existiert", + "not exists": "existiert nicht", + "Style": "Stil", + "=": "=", + "≠": "≠", + ">": ">", + "≥": "≥", + "<": "<", + "≤": "≤", + "Role UID": "Rollen-UID", + "Precision": "Genauigkeit", + "Formula mode": "Formelmodus", + "Expression": "Ausdruck", + "Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Geben Sie +, -, *, /, ( ) zum Berechnen ein, geben Sie @ ein, um Feldvariablen zu öffnen.", + "Formula error.": "Formelfehler.", + "Rich Text": "Rich Text", + "Junction collection": "Verbindungssammlung", + "Leave it blank, unless you need a custom intermediate table": "Lassen Sie es leer, es sei denn, Sie benötigen eine benutzerdefinierte Zwischentabelle", + "Fields": "Felder", + "Edit field title": "Feldtitel bearbeiten", + "Field title": "Feldtitel", + "Original field title: ": "Ursprünglicher Feldtitel: ", + "Edit tooltip": "Tooltip bearbeiten", + "Delete field": "Feld löschen", + "Select collection": "Sammlung auswählen", + "Blank block": "Leerer Block", + "Duplicate template": "Vorlage duplizieren", + "Reference template": "Referenzvorlage", + "Create calendar block": "Kalenderblock erstellen", + "Create kanban block": "Kanban-Block erstellen", + "Grouping field": "Gruppierungsfeld", + "Single select and radio fields can be used as the grouping field": "Einzelauswahl- und Radiofelder können als Gruppierungsfeld verwendet werden", + "Tab name": "Tab-Name", + "Current record blocks": "Blöcke des aktuellen Datensatzes", + "Popup message": "Popup-Nachricht", + "Delete role": "Rolle löschen", + "Role display name": "Rollenanzeigename", + "Default role": "Standardrolle", + "All collections use general action permissions by default; permission configured individually will override the default one.": "Alle Sammlungen verwenden standardmäßig allgemeine Aktionsberechtigungen; individuell konfigurierte Berechtigungen überschreiben die Standardeinstellung.", + "Allows configuration of the whole system, including UI, collections, permissions, etc.": "Ermöglicht die Konfiguration des gesamten Systems, einschließlich UI, Sammlungen, Berechtigungen usw.", + "New menu items are allowed to be accessed by default.": "Neue Menüpunkte dürfen standardmäßig zugegriffen werden.", + "Global permissions": "Globale Berechtigungen", + "General permissions": "Allgemeine Berechtigungen", + "Global action permissions": "Globale Aktionsberechtigungen", + "General action permissions": "Allgemeine Aktionsberechtigungen", + "Plugin settings permissions": "Plugin-Einstellungsberechtigungen", + "Allow to desgin pages": "Erlauben, Seiten zu gestalten", + "Allow to manage plugins": "Erlauben, Plugins zu verwalten", + "Allow to configure plugins": "Erlauben, Plugins zu konfigurieren", + "Allows to configure interface": "Erlaubt die Konfiguration der Schnittstelle", + "Allows to install, activate, disable plugins": "Erlaubt das Installieren, Aktivieren und Deaktivieren von Plugins", + "Allows to configure plugins": "Erlaubt die Konfiguration von Plugins", + "Action display name": "Anzeigeame der Aktion", + "Allow": "Erlauben", + "Data scope": "Datenbereich", + "Action on new records": "Aktion für neue Datensätze", + "Action on existing records": "Aktion für bestehende Datensätze", + "All records": "Alle Datensätze", + "Own records": "Eigene Datensätze", + "Permission policy": "Berechtigungsrichtlinie", + "Individual": "Individuell", + "General": "Allgemein", + "Accessible": "Zugänglich", + "Configure permission": "Berechtigung konfigurieren", + "Action permission": "Aktionsberechtigung", + "Field permission": "Feldberechtigung", + "Scope name": "Bereichsname", + "Unsaved changes": "Ungespeicherte Änderungen", + "Are you sure you don't want to save?": "Sind Sie sicher, dass Sie nicht speichern möchten?", + "Dragging": "Ziehen", + "Popup": "Popup", + "Trigger workflow": "Workflow auslösen", + "Request API": "API anfragen", + "Assign field values": "Feldwerte zuweisen", + "Constant value": "Konstanter Wert", + "Dynamic value": "Dynamischer Wert", + "Current user": "Aktueller Benutzer", + "Current role": "Aktuelle Rolle", + "Current record": "Aktueller Datensatz", + "Current collection": "Aktuelle Sammlung", + "Other collections": "Andere Sammlungen", + "Current popup record": "Aktueller Popup-Datensatz", + "Parent popup record": "Übergeordneter Popup-Datensatz", + "Associated records": "Zugehörige Datensätze", + "Parent record": "Übergeordneter Datensatz", + "Current time": "Aktuelle Zeit", + "System variables": "Systemvariablen", + "Date variables": "Datumsvariablen", + "Message popup close method": "Schließmethode für Popup-Nachrichten", + "Automatic close": "Automatisch schließen", + "Manually close": "Manuell schließen", + "After successful update": "Nach erfolgreicher Aktualisierung", + "Save record": "Datensatz speichern", + "Updated successfully": "Erfolgreich aktualisiert", + "After successful save": "Nach erfolgreichem Speichern", + "After clicking the custom button, the following field values will be assigned according to the following form.": "Nach dem Klicken auf die benutzerdefinierte Schaltfläche werden die folgenden Feldwerte gemäß dem folgenden Formular zugewiesen.", + "After clicking the custom button, the following fields of the current record will be saved according to the following form.": "Nach dem Klicken auf die benutzerdefinierte Schaltfläche werden die folgenden Felder des aktuellen Datensatzes gemäß dem folgenden Formular gespeichert.", + "Button background color": "Schaltflächen-Hintergrundfarbe", + "Highlight": "Hervorheben", + "Danger red": "Gefahr-Rot", + "Custom request": "Benutzerdefinierte Anfrage", + "Request settings": "Anfrageeinstellungen", + "Request URL": "Anfrage-URL", + "Request method": "Anfragemethode", + "Request query parameters": "Anfrageparameter", + "Request headers": "Anfrageheader", + "Request body": "Anfragekörper", + "Request success": "Anfrage erfolgreich", + "Invalid JSON format": "Ungültiges JSON-Format", + "After successful request": "Nach erfolgreicher Anfrage", + "Add exportable field": "Exportierbares Feld hinzufügen", + "Audit logs": "Prüfprotokolle", + "Record ID": "Datensatz-ID", + "User": "Benutzer", + "Field": "Feld", + "Select": "Auswählen", + "Select field": "Feld auswählen", + "Field value changes": "Feldwertänderungen", + "One to one (has one)": "Eins zu Eins (hat ein)", + "One to one (belongs to)": "Eins zu Eins (gehört zu)", + "Use the same time zone (GMT) for all users": "Verwenden Sie die gleiche Zeitzone (GMT) für alle Benutzer", + "Province/city/area name": "Provinz/Stadt/Gebietsname", + "Enabled languages": "Aktivierte Sprachen", + "View all plugins": "Alle Plugins anzeigen", + "Print": "Drucken", + "Done": "Fertig", + "Sign up successfully, and automatically jump to the sign in page": "Registrierung erfolgreich, Sie werden automatisch zur Anmeldeseite weitergeleitet", + "File manager": "Dateimanager", + "ACL": "ACL", + "Collection manager": "Sammlungsmanager", + "Plugin manager": "Plugin-Manager", + "Local": "Lokal", + "Built-in": "Eingebaut", + "Marketplace": "Marktplatz", + "Add plugin": "Plugin hinzufügen", + "Plugin source": "Plugin-Quelle", + "Upgrade": "Aktualisieren", + "Plugin dependencies check failed": "Überprüfung der Plugin-Abhängigkeiten fehlgeschlagen", + "More details": "Weitere Details", + "Upload new version": "Neue Version hochladen", + "Version": "Version", + "Npm package": "NPM-Paket", + "Npm package name": "NPM-Paketname", + "Upload plugin": "Plugin hochladen", + "Official plugin": "Offizielles Plugin", + "Add type": "Typ hinzufügen", + "Changelog": "Änderungsprotokoll", + "Dependencies check": "Abhängigkeitsprüfung", + "Update plugin": "Plugin aktualisieren", + "Installing": "Installiere", + "The deletion was successful.": "Das Löschen war erfolgreich.", + "Plugin Zip File": "Plugin-ZIP-Datei", + "Compressed file url": "URL der komprimierten Datei", + "Last updated": "Zuletzt aktualisiert", + "PackageName": "Paketname", + "DisplayName": "Anzeigename", + "Readme": "Readme", + "Dependencies compatibility check": "Kompatibilitätsprüfung der Abhängigkeiten", + "Plugin dependencies check failed, you should change the dependent version to meet the version requirements.": "Die Überprüfung der Plugin-Abhängigkeiten ist fehlgeschlagen. Sie sollten die abhängige Version ändern, um die Versionsanforderungen zu erfüllen.", + "Version range": "Versionsbereich", + "Plugin's version": "Plugin-Version", + "Result": "Ergebnis", + "No CHANGELOG.md file": "Keine CHANGELOG.md-Datei", + "No README.md file": "Keine README.md-Datei", + "Homepage": "Startseite", + "Drag and drop the file here or click to upload, file size should not exceed 30M": "Ziehen Sie die Datei hierher oder klicken Sie zum Hochladen, die Dateigröße sollte 30M nicht überschreiten", + "Dependencies check failed, can't enable.": "Abhängigkeitsprüfung fehlgeschlagen, kann nicht aktiviert werden.", + "Plugin starting...": "Plugin wird gestartet...", + "Plugin stopping...": "Plugin wird gestoppt...", + "Are you sure to delete this plugin?": "Sind Sie sicher, dass Sie dieses Plugin löschen möchten?", + "Are you sure to disable this plugin?": "Sind Sie sicher, dass Sie dieses Plugin deaktivieren möchten?", + "re-download file": "Datei erneut herunterladen", + "Not enabled": "Nicht aktiviert", + "Search plugin": "Plugin suchen", + "Author": "Autor", + "Plugin loading failed. Please check the server logs.": "Plugin-Ladung fehlgeschlagen. Bitte überprüfen Sie die Serverprotokolle.", + "Coming soon...": "Demnächst verfügbar...", + "All plugin settings": "Alle Plugin-Einstellungen", + "Bookmark": "Lesezeichen", + "Manage all settings": "Alle Einstellungen verwalten", + "Create inverse field in the target collection": "Inverses Feld in der Zielsammlung erstellen", + "Inverse field name": "Name des inversen Feldes", + "Inverse field display name": "Anzeigename des inversen Feldes", + "Bulk update": "Massenaktualisierung", + "After successful bulk update": "Nach erfolgreicher Massenaktualisierung", + "Bulk edit": "Massenbearbeitung", + "Data will be updated": "Daten werden aktualisiert", + "Selected": "Ausgewählt", + "All": "Alle", + "Update selected data?": "Ausgewählte Daten aktualisieren?", + "Update all data?": "Alle Daten aktualisieren?", + "Remains the same": "Bleibt gleich", + "Changed to": "Geändert zu", + "Clear": "Löschen", + "Add attach": "Anhang hinzufügen", + "Please select the records to be updated": "Bitte wählen Sie die zu aktualisierenden Datensätze aus", + "Selector": "Selektor", + "Inner": "Innen", + "Search and select collection": "Sammlung suchen und auswählen", + "Please fill in the iframe URL": "Bitte geben Sie die iframe-URL ein", + "Fix block": "Block fixieren", + "Plugin name": "Plugin-Name", + "Plugin tab name": "Plugin-Tab-Name", + "AutoGenId": "Automatisch generiertes ID-Feld", + "CreatedBy": "Erstellt von", + "UpdatedBy": "Aktualisiert von", + "CreatedAt": "Erstellt am", + "UpdatedAt": "Aktualisiert am", + "Column width": "Spaltenbreite", + "Sortable": "Sortierbar", + "Enable link": "Link aktivieren", + "This is likely a NocoBase internals bug. Please open an issue at <1>here": "Dies ist wahrscheinlich ein interner Fehler von NocoBase. Bitte öffnen Sie ein Problem <1>hier", + "Render Failed": "Rendering fehlgeschlagen", + "App error": "App-Fehler", + "Feedback": "Feedback", + "Try again": "Erneut versuchen", + "Download logs": "Protokolle herunterladen", + "Data template": "Datenvorlage", + "Duplicate": "Duplizieren", + "Duplicating": "Dupliziere", + "Duplicate mode": "Duplikationsmodus", + "Quick duplicate": "Schnelles Duplizieren", + "Duplicate and continue": "Duplizieren und fortfahren", + "Please configure the duplicate fields": "Bitte konfigurieren Sie die Duplikatfelder", + "Add": "Hinzufügen", + "Add new mode": "Neuer Hinzufügungsmodus", + "Quick add": "Schnell hinzufügen", + "Modal add": "Modal hinzufügen", + "Save mode": "Speichermodus", + "First or create": "Zuerst oder erstellen", + "Update or create": "Aktualisieren oder erstellen", + "Find by the following fields": "Nach den folgenden Feldern suchen", + "Create": "Erstellen", + "Current form": "Aktuelles Formular", + "Current object": "Aktuelles Objekt", + "Linkage with form fields": "Verknüpfung mit Formularfeldern", + "Allow add new, update and delete actions": "Hinzufügen, Aktualisieren und Löschen erlauben", + "Date display format": "Datumsanzeigeformat", + "Assign data scope for the template": "Datenbereich für die Vorlage zuweisen", + "Table selected records": "Ausgewählte Tabellendatensätze", + "Tag": "Tag", + "Tag color field": "Tag-Farbfeld", + "Sync successfully": "Synchronisierung erfolgreich", + "Sync from form fields": "Von Formularfeldern synchronisieren", + "Select all": "Alle auswählen", + "Restart": "Neustart", + "Restart application": "Anwendung neu starten", + "Cascade Select": "Kaskadierte Auswahl", + "Execute": "Ausführen", + "Please use a valid SELECT or WITH AS statement": "Bitte verwenden Sie eine gültige SELECT- oder WITH AS-Anweisung", + "Please confirm the SQL statement first": "Bitte bestätigen Sie zuerst die SQL-Anweisung", + "Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "Automatisches Löschen von Objekten, die von der Sammlung abhängen (wie Ansichten), und wiederum aller Objekte, die von diesen Objekten abhängen", + "Sign in with another account": "Mit einem anderen Konto anmelden", + "Return to the main application": "Zurück zur Hauptanwendung", + "Permission deined": "Berechtigung verweigert", + "loading": "Lädt", + "name is required": "Name ist erforderlich", + "data source": "Datenquelle", + "Data source": "Datenquelle", + "DataSource": "Datenquelle", + "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "Der {{type}} \"{{name}}\" wurde möglicherweise gelöscht. Bitte entfernen Sie diesen {{blockType}}.", + "Preset fields": "Voreingestellte Felder", + "Home page": "Startseite", + "Handbook": "Handbuch", + "License": "Lizenz", + "Generic properties": "Allgemeine Eigenschaften", + "Specific properties": "Spezifische Eigenschaften", + "Used for drag and drop sorting scenarios, supporting grouping sorting": "Wird für Drag & Drop-Sortierungsszenarien verwendet und unterstützt Gruppensortierung", + "Grouped sorting": "Gruppierte Sortierung", + "When a field is selected for grouping, it will be grouped first before sorting.": "Wenn ein Feld für die Gruppierung ausgewählt wird, wird es zuerst gruppiert, bevor es sortiert wird.", + "Departments": "Abteilungen", + "Main department": "Hauptabteilung", + "Department name": "Abteilungsname", + "Superior department": "Übergeordnete Abteilung", + "Owners": "Eigentümer", + "Plugin settings": "Plugin-Einstellungen", + "Menu": "Menü", + "Drag and drop sorting field": "Feld für Drag & Drop-Sortierung", + "This variable has been deprecated and can be replaced with \"Current form\"": "Diese Variable ist veraltet und kann durch \"Aktuelles Formular\" ersetzt werden", + "The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "Der Wert dieser Variable wird aus der Abfragezeichenfolge der Seiten-URL abgeleitet. Diese Variable kann nur normal verwendet werden, wenn die Seite eine Abfragezeichenfolge hat.", + "URL search params": "URL-Suchparameter", + "Expand All": "Alle erweitern", + "Search": "Suchen", + "Clear default value": "Standardwert löschen", + "Open in new window": "In neuem Fenster öffnen", + "Sorry, the page you visited does not exist.": "Entschuldigung, die von Ihnen besuchte Seite existiert nicht.", + "is none of": "ist keines von", + "is any of": "ist eines von", + "Plugin dependency version mismatch": "Versionsinkompatibilität der Plugin-Abhängigkeit", + "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "Die aktuelle Abhängigkeitsversion des Plugins stimmt nicht mit der Version der Anwendung überein und funktioniert möglicherweise nicht ordnungsgemäß. Sind Sie sicher, dass Sie das Plugin weiterhin aktivieren möchten?", + "Allow multiple selection": "Mehrfachauswahl erlauben", + "Parent object": "Übergeordnetes Objekt", + "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "Überspringt das Abrufen der Gesamtanzahl der Tabellendatensätze während der Paginierung, um das Laden zu beschleunigen. Es wird empfohlen, diese Option für Datentabellen mit einer großen Datenmenge zu aktivieren", + "Enable secondary confirmation": "Sekundäre Bestätigung aktivieren", + "Notification": "Benachrichtigung", + "Ellipsis overflow content": "Auslassungszeichen für Überlaufinhalt", + "Hide column": "Spalte ausblenden", + "In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "Im Konfigurationsmodus wird die gesamte Spalte transparent. Im Nicht-Konfigurationsmodus wird die gesamte Spalte ausgeblendet. Auch wenn die gesamte Spalte ausgeblendet ist, werden ihre konfigurierten Standardwerte und andere Einstellungen weiterhin wirksam.", + "Unauthenticated. Please sign in to continue.": "Nicht authentifiziert. Bitte melden Sie sich an, um fortzufahren.", + "User not found. Please sign in again to continue.": "Benutzer nicht gefunden. Bitte melden Sie sich erneut an, um fortzufahren.", + "Your session has expired. Please sign in again.": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.", + "User password changed, please signin again.": "Benutzerpasswort geändert, bitte melden Sie sich erneut an.", + "Desktop routes": "Desktop-Routen", + "Route permissions": "Routenberechtigungen", + "New routes are allowed to be accessed by default": "Neue Routen dürfen standardmäßig zugegriffen werden", + "Route name": "Routenname", + "Mobile routes": "Mobile Routen", + "Show in menu": "Im Menü anzeigen", + "Hide in menu": "Im Menü ausblenden", + "Path": "Pfad", + "Type": "Typ", + "Access": "Zugriff", + "Routes": "Routen", + "Add child route": "Unterroute hinzufügen", + "Delete routes": "Routen löschen", + "Delete route": "Route löschen", + "Are you sure you want to hide these routes in menu?": "Sind Sie sicher, dass Sie diese Routen im Menü ausblenden möchten?", + "Are you sure you want to show these routes in menu?": "Sind Sie sicher, dass Sie diese Routen im Menü anzeigen möchten?", + "Are you sure you want to hide this menu?": "Sind Sie sicher, dass Sie dieses Menü ausblenden möchten?", + "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Nach dem Ausblenden wird dieses Menü nicht mehr in der Menüleiste angezeigt. Um es wieder anzuzeigen, müssen Sie zur Routenverwaltungsseite gehen, um es zu konfigurieren.", + "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." +} diff --git a/packages/core/client/src/locale/en-US.json b/packages/core/client/src/locale/en-US.json index b7a526e23a..adea6f965f 100644 --- a/packages/core/client/src/locale/en-US.json +++ b/packages/core/client/src/locale/en-US.json @@ -476,6 +476,8 @@ "Action permissions": "Action permissions", "Menu permissions": "Menu permissions", "Menu item name": "Menu item name", + "Input value": "Input value", + "Output value": "Output value", "Allow access": "Allow access", "Action name": "Action name", "Allow action": "Allow action", @@ -884,7 +886,10 @@ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.", "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.", "Deprecated": "Deprecated", "The following old template features have been deprecated and will be removed in next version.": "The following old template features have been deprecated and will be removed in next version.", - "Array": "Array" + "Array": "Array", + "Full permissions": "Full permissions" } diff --git a/packages/core/client/src/locale/es-ES.json b/packages/core/client/src/locale/es-ES.json index 2bd4e25ad4..2ac8396c0c 100644 --- a/packages/core/client/src/locale/es-ES.json +++ b/packages/core/client/src/locale/es-ES.json @@ -801,6 +801,9 @@ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Después de ocultar, este menú ya no aparecerá en la barra de menú. Para mostrarlo de nuevo, debe ir a la página de administración de rutas para configurarlo.", "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.", "Deprecated": "Obsoleto", - "The following old template features have been deprecated and will be removed in next version.": "Las siguientes características de plantilla antigua han quedado obsoletas y se eliminarán en la próxima versión." + "The following old template features have been deprecated and will be removed in next version.": "Las siguientes características de plantilla antigua han quedado obsoletas y se eliminarán en la próxima versión.", + "Full permissions": "Todos los derechos" } diff --git a/packages/core/client/src/locale/fr-FR.json b/packages/core/client/src/locale/fr-FR.json index c8864c011d..2d6e12101c 100644 --- a/packages/core/client/src/locale/fr-FR.json +++ b/packages/core/client/src/locale/fr-FR.json @@ -821,6 +821,9 @@ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Après avoir masqué, ce menu ne sera plus affiché dans la barre de menu. Pour le réafficher, vous devez aller à la page de gestion des routes pour le configurer.", "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.", "Deprecated": "Déprécié", - "The following old template features have been deprecated and will be removed in next version.": "Les fonctionnalités des anciens modèles ont été dépréciées et seront supprimées dans la prochaine version." + "The following old template features have been deprecated and will be removed in next version.": "Les fonctionnalités des anciens modèles ont été dépréciées et seront supprimées dans la prochaine version.", + "Full permissions": "Tous les droits" } diff --git a/packages/core/client/src/locale/it-IT.json b/packages/core/client/src/locale/it-IT.json index ed4c1bdb13..e2c2e47e53 100644 --- a/packages/core/client/src/locale/it-IT.json +++ b/packages/core/client/src/locale/it-IT.json @@ -116,7 +116,7 @@ "Table": "Tabella", "Table OID(Inheritance)": "Tabella OID (eredità)", "Form": "Modulo", - "List": "Lista", + "List": "Elenco", "Grid Card": "Scheda griglia", "pixels": "pixel", "Screen size": "Dimensione dello schermo", @@ -127,7 +127,7 @@ "Tablet device": "Tablet", "Desktop device": "Desktop", "Large screen device": "Schermo di grandi dimensioni", - "Collapse": "Collassa", + "Collapse": "Comprimi", "Select data source": "Seleziona origine dati", "Calendar": "Calendario", "Delete events": "Elimina eventi", @@ -171,10 +171,10 @@ "Add new": "Aggiungi nuovo", "Add record": "Aggiungi record", "Add child": "Aggiungi figlio", - "Collapse all": "Collassare tutto", - "Expand all": "Espandere tutto", - "Expand/Collapse": "Espandere/Collassare", - "Default collapse": "Collassa di default", + "Collapse all": "Comprimi tutto", + "Expand all": "Espandi tutto", + "Expand/Collapse": "Espandi/Comprimi", + "Default collapse": "Comprimi di default", "Tree table": "Tabella ad albero", "Custom field display name": "Nome visualizzato campo personalizzato ", "Display fields": "Visualizza campi", @@ -276,7 +276,7 @@ "Created by": "Creato da", "Last updated by": "Ultimo aggiornamento da", "Add field": "Aggiungi campo", - "Field display name": "Nome visualizzazione campo", + "Field display name": "Nome visualizzato campo", "Field type": "Tipo campo", "Field interface": "Interfaccia campo", "Date format": "Formato data", @@ -344,7 +344,7 @@ "Edit chart": "Modifica grafico", "Add text": "Aggiungi testo", "Filterable fields": "Campi filtrabili", - "Edit button": "Pulsante Modifica", + "Edit button": "Modifica pulsante", "Hide": "Nascondi", "Enable actions": "Abilita operazioni", "Import": "Importa", @@ -552,7 +552,7 @@ "Fields values": "Valori campi", "The field has been deleted": "Il campo è stato eliminato", "When submitting the following fields, the saved values are": "Quando si inviano i seguenti campi, i valori salvati sono", - "After successful submission": "Dopo una invio riuscito", + "After successful submission": "Dopo un invio riuscito", "Then": "Poi", "Stay on current page": "Resta sulla pagina corrente", "Redirect to": "Reindirizza a", @@ -612,7 +612,7 @@ "Allows to configure interface": "Consente di configurare l'interfaccia", "Allows to install, activate, disable plugins": "Consente di installare, attivare, disabilitare i plugin", "Allows to configure plugins": "Consente di configurare i plugin", - "Action display name": "Nome visualizzazione azione", + "Action display name": "Nome visualizzato operazione", "Allow": "Permetti", "Data scope": "Ambito dei dati", "Action on new records": "Operazione su nuovi record", @@ -658,9 +658,9 @@ "After clicking the custom button, the following field values will be assigned according to the following form.": "Dopo aver fatto clic sul pulsante personalizza, i seguenti valori verranno assegnati in base al seguente modulo.", "After clicking the custom button, the following fields of the current record will be saved according to the following form.": "Dopo aver fatto clic sul pulsante personalizza, i seguenti campi del record corrente verranno salvati in base al seguente modulo.", "Button background color": "Colore sfondo del pulsante", - "Highlight": "Evidenzia", - "Danger red": "Pericolo rosso", - "Custom request": "Personalizza richiesta", + "Highlight": "Evidenziato", + "Danger red": "Rosso pericolo", + "Custom request": "Richiesta personalizzata", "Request settings": "Impostazioni richiesta", "Request URL": "URL richiesta", "Request method": "Metodo richiesta", @@ -742,7 +742,7 @@ "Manage all settings": "Gestisci tutte le impostazioni", "Create inverse field in the target collection": "Crea campo inverso nella raccolta di destinazione", "Inverse field name": "Nome campo inverso", - "Inverse field display name": "Nome visualizzazione campo inverso", + "Inverse field display name": "Nome visualizzato campo inverso", "Bulk update": "Aggiornamento di massa", "After successful bulk update": "Dopo un aggiornamento di massa riuscito", "Bulk edit": "Modifica di massa", @@ -753,7 +753,7 @@ "Update all data?": "Aggiornare tutti i dati?", "Remains the same": "Rimane lo stesso", "Changed to": "Cambiato in", - "Clear": "Pulisci", + "Clear": "Cancella", "Add attach": "Aggiungi allegato", "Please select the records to be updated": "Si prega di selezionare i record da aggiornare", "Selector": "Selettore", @@ -796,7 +796,7 @@ "Current form": "Modulo corrente", "Current object": "Oggetto corrente", "Linkage with form fields": "Collegamento con i campi del modulo", - "Allow add new, update and delete actions": "Consenti aggiungi nuovo, aggiorna ed elimina", + "Allow add new, update and delete actions": "Consenti operazioni aggiungi nuovo, aggiorna ed elimina", "Date display format": "Formato di visualizzazione della data", "Assign data scope for the template": "Assegna l'ambito dei dati per il modello", "Table selected records": "Tabella record selezionati", @@ -841,7 +841,7 @@ "This variable has been deprecated and can be replaced with \"Current form\"": "Questa variabile è stata deprecata e può essere sostituita con \"Current form\"", "The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "Il valore di questa variabile deriva dalla stringa di ricerca nell'URL della pagina. Questa variabile può essere utilizzata normalmente solo quando la pagina ha una stringa di ricerca.", "URL search params": "Parametri di ricerca URL", - "Expand All": "Espandere tutto", + "Expand All": "Espandi tutto", "Search": "Ricerca", "Clear default value": "Cancella il valore predefinito", "Open in new window": "Apri in una nuova finestra", @@ -857,5 +857,6 @@ "Notification": "Notifica", "Ellipsis overflow content": "Contenuto Ellipsis overflow", "Hide column": "Nascondi colonna", - "In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "In modalità di configurazione, l'intera colonna diventa trasparente. In modalità non di configurazione, l'intera colonna verrà nascosta. Anche se l'intera colonna è nascosta, i suoi valori predefiniti configurati e le altre impostazioni avranno comunque effetto." -} \ No newline at end of file + "In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "In modalità di configurazione, l'intera colonna diventa trasparente. In modalità non di configurazione, l'intera colonna verrà nascosta. Anche se l'intera colonna è nascosta, i suoi valori predefiniti configurati e le altre impostazioni avranno comunque effetto.", + "The following old template features have been deprecated and will be removed in next version.": "Le seguenti funzionalità dei modelli vecchi sono state deprecate e saranno rimosse nella prossima versione." +} diff --git a/packages/core/client/src/locale/ja-JP.json b/packages/core/client/src/locale/ja-JP.json index 4fd2c9053c..1298a1f9c6 100644 --- a/packages/core/client/src/locale/ja-JP.json +++ b/packages/core/client/src/locale/ja-JP.json @@ -1039,6 +1039,9 @@ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "非表示にすると、このメニューはメニューバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。", "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.": "非表示にすると、このタブはタブバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。", "Deprecated": "非推奨", - "The following old template features have been deprecated and will be removed in next version.": "次の古いテンプレート機能は非推奨になり、次のバージョンで削除されます。" + "The following old template features have been deprecated and will be removed in next version.": "次の古いテンプレート機能は非推奨になり、次のバージョンで削除されます。", + "Full permissions": "すべての権限" } diff --git a/packages/core/client/src/locale/ko-KR.json b/packages/core/client/src/locale/ko-KR.json index 57b261ef57..8e0d686008 100644 --- a/packages/core/client/src/locale/ko-KR.json +++ b/packages/core/client/src/locale/ko-KR.json @@ -912,6 +912,9 @@ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "숨기면 이 메뉴는 메뉴 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다.", "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.": "숨기면 이 탭은 탭 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다.", "Deprecated": "사용 중단됨", - "The following old template features have been deprecated and will be removed in next version.": "다음 오래된 템플릿 기능은 사용 중단되었으며 다음 버전에서 제거될 것입니다." + "The following old template features have been deprecated and will be removed in next version.": "다음 오래된 템플릿 기능은 사용 중단되었으며 다음 버전에서 제거될 것입니다.", + "Full permissions": "모든 권한" } diff --git a/packages/core/client/src/locale/nl-NL.json b/packages/core/client/src/locale/nl-NL.json new file mode 100644 index 0000000000..ade6cca425 --- /dev/null +++ b/packages/core/client/src/locale/nl-NL.json @@ -0,0 +1,888 @@ +{ + "Display <1><0>10<1>20<2>50<3>100 items per page": "Toon <1><0>10<1>20<2>50<3>100 items per pagina", + "Meet <1><0>All<1>Any conditions in the group": "Voldoe aan <1><0>Alle<1>Een voorwaarde(n) in de groep", + "Open in<1><0>Modal<1>Drawer<2>Window": "Open in<1><0>Modal<1>Drawer<2>Venster", + "{{count}} filter items": "{{count}} filter items", + "{{count}} more items": "{{count}} meer items", + "Total {{count}} items": "Totaal {{count}} items", + "Today": "Vandaag", + "Yesterday": "Gisteren", + "Tomorrow": "Morgen", + "Month": "Maand", + "Week": "Week", + "This week": "Deze week", + "This month": "Deze maand", + "This year": "Dit jaar", + "Next year": "Volgend jaar", + "Last week": "Vorige week", + "Next week": "Volgende week", + "Last month": "Vorige maand", + "Next month": "Volgende maand", + "Last quarter": "Vorig kwartaal", + "This quarter": "Dit kwartaal", + "Next quarter": "Volgend kwartaal", + "Last year": "Vorig jaar", + "Last 7 days": "Laatste 7 dagen", + "Last 30 days": "Laatste 30 dagen", + "Last 90 days": "Laatste 90 dagen", + "Next 7 days": "Volgende 7 dagen", + "Next 30 days": "Volgende 30 dagen", + "Next 90 days": "Volgende 90 dagen", + "Work week": "Werkweek", + "Day": "Dag", + "Agenda": "Agenda", + "Date": "Datum", + "Time": "Tijd", + "Event": "Gebeurtenis", + "None": "Geen", + "Unconnected": "Niet verbonden", + "System settings": "Systeeminstellingen", + "System title": "Systeemtitel", + "Settings": "Instellingen", + "Logo": "Logo", + "Add menu item": "Menu-item toevoegen", + "Page": "Pagina", + "Name": "Naam", + "Icon": "Icoon", + "Group": "Groep", + "Link": "Link", + "Tab": "Tab", + "Save conditions": "Voorwaarden opslaan", + "Edit menu item": "Menu-item bewerken", + "Move to": "Verplaats naar", + "Insert left": "Links invoegen", + "Insert right": "Rechts invoegen", + "Insert inner": "Binnenkant invoegen", + "Delete": "Verwijder", + "Disassociate": "Loskoppelen", + "Disassociate record": "Record loskoppelen", + "Are you sure you want to disassociate it?": "Weet je zeker dat je het wil loskoppelen?", + "UI editor": "UI-editor", + "Collection": "Collectie", + "Collection selector": "Collectie selector", + "Providing certain collections as options for users, typically used in polymorphic or inheritance scenarios": "Bepaalde collecties aanbieden als opties voor gebruikers, meestal gebruikt in polymorfe of erfelijkheidsscenario's", + "Collections & Fields": "Collecties & Velden", + "All collections": "Alle collecties", + "Add category": "Categorie toevoegen", + "Enable child collections": "Onderliggende collecties inschakelen", + "Allow adding records to the current collection": "Toestaan ​​dat records aan de huidige collectie worden toegevoegd", + "Delete category": "Categorie verwijderen", + "Edit category": "Categorie bewerken", + "Collection category": "Collectie categorie", + "Collection template": "Collectie sjabloon", + "Sort": "Sorteren", + "Categories": "Categorieën", + "Visible": "Zichtbaar", + "Read only": "Alleen-lezen", + "Easy reading": "Gemakkelijk te lezen", + "Hidden": "Verborgen", + "Hidden(reserved value)": "Verborgen (gereserveerde waarde)", + "Not required": "Niet vereist", + "Value": "Waarde", + "Disabled": "Uitgeschakeld", + "Enabled": "Ingeschakeld", + "Problematic": "Problematisch", + "Setting": "Instelling", + "On": "Aan", + "Off": "Uit", + "Empty": "Leeg", + "Linkage rule": "Koppelingregel", + "Linkage rules": "Koppelingregels", + "Condition": "Voorwaarde", + "Properties": "Eigenschappen", + "Add linkage rule": "Koppelingregel toevoegen", + "Add property": "Eigenschap toevoegen", + "Category name": "Categorie naam", + "Roles & Permissions": "Rollen & Machtigingen", + "Edit profile": "Profiel bewerken", + "Change password": "Wachtwoord wijzigen", + "Old password": "Oud wachtwoord", + "New password": "Nieuw wachtwoord", + "Switch role": "Rol wisselen", + "Super admin": "Superbeheerder", + "Language": "Taal", + "Allow sign up": "Registratie toestaan", + "Enable SMS authentication": "SMS-authenticatie inschakelen", + "Sign out": "Afmelden", + "Cancel": "Annuleren", + "Submit": "Indienen", + "Close": "Sluiten", + "Set the data scope": "Stel de gegevensomvang in", + "Set data loading mode": "Stel de gegevenslaadmodus in", + "Load all data when filter is empty": "Laad alle gegevens wanneer filter leeg is", + "Do not load data when filter is empty": "Laad geen gegevens wanneer filter leeg is", + "Data loading mode": "Gegevenslaadmodus", + "Data blocks": "Gegevensblokken", + "Filter blocks": "Filterblokken", + "Table": "Tabel", + "Table OID(Inheritance)": "Tabel OID (Erfelijkheid)", + "Form": "Formulier", + "List": "Lijst", + "Grid Card": "Rasterkaart", + "pixels": "pixels", + "Screen size": "Schermgrootte", + "Display title": "Titel weergeven", + "Set the count of columns displayed in a row": "Stel het aantal kolommen in dat in een rij wordt weergegeven", + "Column": "Kolom", + "Phone device": "Telefoonapparaat", + "Tablet device": "Tabletapparaat", + "Desktop device": "Desktopapparaat", + "Large screen device": "Groot schermapparaat", + "Collapse": "Inklappen", + "Select data source": "Gegevensbron selecteren", + "Calendar": "Kalender", + "Delete events": "Gebeurtenissen verwijderen", + "This event": "Deze gebeurtenis", + "This and following events": "Deze en volgende gebeurtenissen", + "All events": "Alle gebeurtenissen", + "Delete this event?": "Deze gebeurtenis verwijderen?", + "Delete Event": "Verwijder gebeurtenis", + "Kanban": "Kanban", + "Gantt": "Gantt", + "Create gantt block": "Maak Gantt-blok", + "Progress field": "Voortgangsveld", + "Time scale": "Tijdschaal", + "Hour": "Uur", + "Quarter of day": "Kwartaal van de dag", + "Half of day": "Helft van de dag", + "Year": "Jaar", + "QuarterYear": "Kwartaaljaar", + "Select grouping field": "Selecteer groeperingsveld", + "Media": "Media", + "Markdown": "Markdown", + "Wysiwyg": "Wysiwyg", + "Chart blocks": "Grafiekblokken", + "Column chart": "Kolomgrafiek", + "Bar chart": "Staafgrafiek", + "Line chart": "Lijngrafiek", + "Pie chart": "Taartgrafiek", + "Area chart": "Oppervlaktegrafiek", + "Other chart": "Andere grafiek", + "Other blocks": "Andere blokken", + "In configuration": "In configuratie", + "Chart title": "Grafiektitel", + "Chart type": "Grafiektype", + "Chart config": "Grafiekconfiguratie", + "Templates": "Sjablonen", + "Select template": "Selecteer sjabloon", + "Action logs": "Actielogs", + "Create template": "Sjabloon maken", + "Edit markdown": "Bewerk markdown", + "Add block": "Blok toevoegen", + "Add new": "Nieuw toevoegen", + "Add record": "Record toevoegen", + "Add child": "Onderliggend toevoegen", + "Collapse all": "Alles inklappen", + "Expand all": "Alles uitbreiden", + "Expand/Collapse": "Uitbreiden/Inklappen", + "Default collapse": "Standaard inklappen", + "Tree table": "Boomtabel", + "Custom field display name": "Aangepaste veldweergavenaam", + "Display fields": "Toon collectie velden", + "Edit record": "Record bewerken", + "Delete menu item": "Menu-item verwijderen", + "Add page": "Pagina toevoegen", + "Add group": "Groep toevoegen", + "Add link": "Link toevoegen", + "Insert above": "Boven invoegen", + "Insert below": "Onder invoegen", + "Save": "Opslaan", + "Delete block": "Blok verwijderen", + "Are you sure you want to delete it?": "Weet je zeker dat je het wil verwijderen?", + "This is a demo text, **supports Markdown syntax**.": "Dit is een demo tekst, **ondersteunt Markdown syntax**.", + "Filter": "Filter", + "Connect data blocks": "Verbind gegevensblokken", + "Action type": "Actietype", + "Actions": "Acties", + "Insert": "Invoegen", + "Insert if not exists": "Invoegen als het niet bestaat", + "Insert if not exists, or update": "Invoegen als het niet bestaat, of bijwerken", + "Determine whether a record exists by the following fields": "Bepaal of een record bestaat op basis van de volgende velden", + "Update": "Bijwerken", + "Update record": "Record bijwerken", + "View": "Bekijken", + "View record": "Record bekijken", + "Refresh": "Vernieuwen", + "Data changes": "Gegevenswijzigingen", + "Field name": "Veldnaam", + "Before change": "Voor wijziging", + "After change": "Na wijziging", + "Delete record": "Record verwijderen", + "Delete collection": "Collectie verwijderen", + "Create collection": "Collectie maken", + "Collection display name": "Weergavenaam collectie", + "Collection name": "Naam collectie", + "Inherits": "Erft", + "Primary key, unique identifier, self growth": "Primaire sleutel, unieke identifier, zelfgroei", + "Store the creation user of each record": "Sla de gebruiker die de record aanmaakte op", + "Store the last update user of each record": "Sla de gebruiker van de laatste update op", + "Store the creation time of each record": "Sla de creatietijd van elke record op", + "Store the last update time of each record": "Sla de laatste update tijd van elke record op", + "More options": "Meer opties", + "Records can be sorted": "Records kunnen worden gesorteerd", + "Calendar collection": "Kalendercollectie", + "General collection": "Algemene collectie", + "Connect to database view": "Verbind met databaseweergave", + "Sync from database": "Synchroniseren vanuit database", + "Source collections": "Broncollecties", + "Field source": "Veldbron", + "Preview": "Voorbeeld", + "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Willekeurig gegenereerd en kan worden aangepast. Ondersteunt letters, cijfers en onderstrepingstekens, moet beginnen met een letter.", + "Edit": "Bewerken", + "Edit collection": "Collectie bewerken", + "Configure fields": "Velden configureren", + "Configure columns": "Kolommen configureren", + "Edit field": "Veld bewerken", + "Override": "Overschrijven", + "Override field": "Veld overschrijven", + "Configure fields of {{title}}": "Configureer velden van {{title}}", + "Association fields filter": "Filter voor associatievelden", + "PK & FK fields": "PK & FK velden", + "Association fields": "Associatievelden", + "Choices fields": "Keuzevelden", + "System fields": "Systeemvelden", + "General fields": "Algemene velden", + "Inherited fields": "Geërfde velden", + "Parent collection fields": "Velden van bovenliggende collectie", + "Basic": "Basis", + "Single line text": "Enkele regel tekst", + "Long text": "Lange tekst", + "Phone": "Telefoon", + "Email": "E-mail", + "Number": "Nummer", + "Integer": "Geheel getal", + "Percent": "Percentage", + "Password": "Wachtwoord", + "Advanced type": "Geavanceerd", + "Formula": "Formule", + "Formula description": "Bereken een waarde in elk record op basis van andere velden in hetzelfde record.", + "Choices": "Keuzes", + "Checkbox": "Selectievak", + "Single select": "Enkele selectie", + "Multiple select": "Meerdere selecteren", + "Radio group": "Radiogroep", + "Checkbox group": "Checkboxgroep", + "China region": "China regio", + "Date & Time": "Datum & Tijd", + "Datetime": "Datumtijd", + "Relation": "Relatie", + "Link to": "Koppel aan", + "Link to description": "Gebruikt om snel collectie-relaties te maken en compatibel met de meest voorkomende scenario's. Geschikt voor niet-ontwikkelaars. Wanneer aanwezig als veld, is het een keuzelijst die wordt gebruikt om records uit de doelcollectie te selecteren. Eenmaal aangemaakt, genereert het tegelijkertijd de bijbehorende velden van de huidige collectie in de doelcollectie.", + "Sub-table": "Subtabel", + "Sub-details": "Subdetails", + "Sub-form(Popover)": "Subformulier (Popover)", + "System info": "Systeeminfo", + "Created at": "Aangemaakt op", + "Last updated at": "Laatst bijgewerkt op", + "Created by": "Aangemaakt door", + "Last updated by": "Laatst bijgewerkt door", + "Add field": "Veld toevoegen", + "Field display name": "Weergavenaam veld", + "Field type": "Veldtype", + "Field interface": "Veldinterface", + "Date format": "Datumformaat", + "Year/Month/Day": "Jaar/Maand/Dag", + "Year-Month-Day": "Jaar-Maand-Dag", + "Day/Month/Year": "Dag/Maand/Jaar", + "Show time": "Toon tijd", + "Time format": "Tijdformaat", + "12 hour": "12 uur", + "24 hour": "24 uur", + "Relationship type": "Relatietype", + "Inverse relationship type": "Omgekeerd relatietype", + "Source collection": "Broncollectie", + "Source key": "Bron sleutel", + "Target collection": "Doelcollectie", + "Through collection": "Via collectie", + "Target key": "Doel sleutel", + "Foreign key": "Buitenlandse sleutel", + "One to one": "Een-op-een", + "One to many": "Een-op-veel", + "Many to one": "Veel-op-een", + "Many to many": "Veel-op-veel", + "Foreign key 1": "Buitenlandse sleutel 1", + "Foreign key 2": "Buitenlandse sleutel 2", + "One to one description": "Gebruikt om een-op-een relaties te maken. For example, a user has a profile.", + "One to many description": "Gebruikt om een een-op-veel-relatie te maken. Bijvoorbeeld, een land heeft veel steden en een stad kan slechts in één land zijn. Wanneer aanwezig als veld, is het een sub-tabel die de records van de geassocieerde collectie weergeeft. Wanneer aangemaakt, wordt automatisch een Veel-op-een veld gegenereerd in de geassocieerde collectie.", + "Many to one description": "Gebruikt om een veel-op-een-relatie te maken. Bijvoorbeeld, een stad kan slechts tot één land behoren en een land kan veel steden hebben. Wanneer aanwezig als veld, is het een keuzelijst die wordt gebruikt om records uit de geassocieerde collectie te selecteren. Eenmaal aangemaakt, wordt automatisch een Een-op-veel veld gegenereerd in de geassocieerde collectie.", + "Many to many description": "Gebruikt om veel-op-veel-relaties te maken. Bijvoorbeeld, een student heeft veel leraren en een leraar heeft veel studenten. Wanneer aanwezig als veld, is het een keuzelijst die wordt gebruikt om records uit de geassocieerde collectie te selecteren.", + "Generated automatically if left blank": "Wordt automatisch gegenereerd als het leeg wordt gelaten", + "Display association fields": "Associatievelden tonen", + "Display field title": "Veldtitel tonen", + "Field component": "Veldcomponent", + "Allow multiple": "Meerdere toestaan", + "Quick upload": "Snelle upload", + "Select file": "Bestand selecteren", + "Subtable": "Subtabel", + "Sub-form": "Subformulier", + "Field mode": "Veldmodus", + "Allow add new data": "Sta toe nieuwe data toe te voegen", + "Record picker": "Selecteer record", + "Toggles the subfield mode": "Wisselt de subveldmodus", + "Selector mode": "Selectormodus", + "Subtable mode": "Subtabel modus", + "Subform mode": "Subformulier modus", + "Edit block title": "Bloktitel bewerken", + "Block title": "Bloktitel", + "Pattern": "Patroon", + "Operator": "Operator", + "Editable": "Bewerkbaar", + "Readonly": "Alleen-lezen", + "Easy-reading": "Gemakkelijk te lezen", + "Add filter": "Filter toevoegen", + "Add filter group": "Filtergroep toevoegen", + "Comparision": "Vergelijking", + "is": "is", + "is not": "is niet", + "contains": "bevat", + "does not contain": "bevat niet", + "starts with": "begint met", + "not starts with": "begint niet met", + "ends with": "eindigt met", + "not ends with": "eindigt niet met", + "is empty": "is leeg", + "is not empty": "is niet leeg", + "Edit chart": "Bewerk grafiek", + "Add text": "Voeg tekst toe", + "Filterable fields": "Filterbare velden", + "Edit button": "Bewerk knop", + "Hide": "Verbergen", + "Enable actions": "Zet acties aan", + "Import": "Importeer", + "Export": "Exporteer", + "Customize": "Pas aan", + "Custom": "Aangepast", + "Function": "Functie", + "Popup form": "Pop-up formulier", + "Flexible popup": "Flexibele pop-up", + "Configure actions": "Configureer acties", + "Display order number": "Toon volgnummer", + "Enable drag and drop sorting": "Zet drag and drop sorteren aan", + "Triggered when the row is clicked": "Geactiveerd wanneer de rij wordt aangeklikt", + "Add tab": "Voeg tabblad toe", + "Disable tabs": "Schakel tabbladen uit", + "Details": "Details", + "Edit form": "Bewerk formulier", + "Create form": "Maak formulier", + "Form (Edit)": "Formulier (Bewerken)", + "Form (Add new)": "Formulier (Nieuw toevoegen)", + "Edit tab": "Bewerk tabblad", + "Relationship blocks": "Relatieblokken", + "Select record": "Selecteer record", + "Display name": "Weergavenaam", + "Select icon": "Selecteer icoon", + "Custom column name": "Aangepaste kolomnaam", + "Edit description": "Bewerk beschrijving", + "Required": "Verplicht", + "Unique": "Uniek", + "Primary": "Primaire", + "Auto increment": "Auto-increment", + "Label field": "Label veld", + "Default is the ID field": "Standaard is het ID-veld", + "Set default sorting rules": "Stel standaard sorteerrregels in", + "Set validation rules": "Stel validatieregels in", + "Max length": "Maximale lengte", + "Min length": "Minimale lengte", + "Maximum": "Maximum", + "Minimum": "Minimum", + "Max length must greater than min length": "Maximale lengte moet groter zijn dan minimale lengte", + "Min length must less than max length": "Minimale lengte moet kleiner zijn dan maximale lengte", + "Maximum must greater than minimum": "Maximum moet groter zijn dan minimum", + "Minimum must less than maximum": "Minimum moet kleiner zijn dan maximum", + "Validation rule": "Validatieregel", + "Add validation rule": "Voeg validatieregel toe", + "Format": "Formaat", + "Regular expression": "Patroon", + "Error message": "Foutmelding", + "Length": "Lengte", + "The field value cannot be greater than ": "De veldwaarde mag niet groter zijn dan ", + "The field value cannot be less than ": "De veldwaarde mag niet kleiner zijn dan ", + "The field value is not an integer number": "De veldwaarde is geen geheel getal", + "Set default value": "Stel standaardwaarde in", + "Default value": "Standaardwaarde", + "is before": "is voor", + "is after": "is na", + "is on or after": "is op of na", + "is on or before": "is op of voor", + "is between": "is tussen", + "Upload": "Uploaden", + "Select level": "Selecteer niveau", + "Province": "Provincie", + "City": "Stad", + "Area": "Gebied", + "Street": "Straat", + "Village": "Dorp", + "Must select to the last level": "Moet tot het laatste niveau selecteren", + "Move {{title}} to": "Verplaats {{title}} naar", + "Target position": "Doelpositie", + "After": "Na", + "Before": "Voor", + "Add {{type}} before \"{{title}}\"": "Voeg {{type}} toe voor \"{{title}}\"", + "Add {{type}} after \"{{title}}\"": "Voeg {{type}} toe na \"{{title}}\"", + "Add {{type}} in \"{{title}}\"": "Voeg {{type}} toe in \"{{title}}\"", + "Original name": "Originele naam", + "Custom name": "Aangepaste naam", + "Custom Title": "Aangepaste titel", + "Options": "Opties", + "Option value": "Optiewaarde", + "Option label": "Optielabel", + "Color": "Kleur", + "Background Color": "Achtergrondkleur", + "Text Align": "Tekstuitlijning", + "Add option": "Optie toevoegen", + "Related collection": "Gerelateerde collectie", + "Allow linking to multiple records": "Koppelen aan meerdere records toestaan", + "Allow uploading multiple files": "Meerdere bestanden uploaden toestaan", + "Configure calendar": "Kalender configureren", + "Title field": "Titelfeld", + "Custom title": "Aangepaste titel", + "Daily": "Dagelijks", + "Weekly": "Wekelijks", + "Monthly": "Maandelijks", + "Yearly": "Jaarlijks", + "Repeats": "Herhalingen", + "Show lunar": "Maankalender tonen", + "Start date field": "Begindatumveld", + "End date field": "Einddatumveld", + "Navigate": "Navigeren", + "Title": "Titel", + "Description": "Beschrijving", + "Select view": "Weergave selecteren", + "Reset": "Resetten", + "Importable fields": "Importeerbare velden", + "Exportable fields": "Exporteerbare velden", + "Saved successfully": "Succesvol opgeslagen", + "Nickname": "Bijnaam", + "Sign in": "Inloggen", + "Sign in via account": "Inloggen via account", + "Sign in via phone": "Inloggen via telefoon", + "Create an account": "Account aanmaken", + "Sign up": "Registreren", + "Confirm password": "Wachtwoord bevestigen", + "Log in with an existing account": "Inloggen met een bestaand account", + "Signed up successfully. It will jump to the login page.": "Succesvol geregistreerd. Je wordt doorgestuurd naar de inlogpagina.", + "Password mismatch": "Wachtwoorden komen niet overeen", + "Users": "Gebruikers", + "Verification code": "Verificatiecode", + "Send code": "Code verzenden", + "Retry after {{count}} seconds": "Probeer opnieuw over {{count}} seconden", + "Roles": "Rollen", + "Add role": "Rol toevoegen", + "Role name": "Rolnaam", + "Configure": "Configureren", + "Configure permissions": "Rechten configureren", + "Edit role": "Rol bewerken", + "Action permissions": "Actierechten", + "Menu permissions": "Menurechten", + "Menu item name": "Menunaam", + "Allow access": "Toegang toestaan", + "Action name": "Actienaam", + "Allow action": "Actie toestaan", + "Action scope": "Actieomvang", + "Operate on new data": "Werken met nieuwe data", + "Operate on existing data": "Werken met bestaande data", + "Yes": "Ja", + "No": "Nee", + "Red": "Rood", + "Magenta": "Magenta", + "Volcano": "Vulkaan", + "Orange": "Oranje", + "Gold": "Goud", + "Lime": "Limoen", + "Green": "Groen", + "Cyan": "Cyaan", + "Blue": "Blauw", + "Geek blue": "Geek blauw", + "Purple": "Paars", + "Default": "Standaard", + "Add card": "Kaart toevoegen", + "edit title": "titel bewerken", + "Turn pages": "Pagina's omslaan", + "Others": "Overigen", + "Other records": "Andere records", + "Save as template": "Opslaan als sjabloon", + "Save as block template": "Opslaan als bloksjabloon", + "Block templates": "Bloksjablonen", + "Block template": "Bloksjabloon", + "Convert reference to duplicate": "Referentie omzetten naar duplicaat", + "Template name": "Sjabloonnaam", + "Block type": "Bloktype", + "No blocks to connect": "Geen blokken om te verbinden", + "Action column": "Actiekolom", + "Records per page": "Records per pagina", + "(Fields only)": "(Alleen velden)", + "Button title": "Knoptekst", + "Button icon": "Knoppictogram", + "Submitted successfully": "Succesvol ingediend", + "Operation succeeded": "Bewerking geslaagd", + "Operation failed": "Bewerking mislukt", + "Open mode": "Openingsmodus", + "Popup size": "Popupgrootte", + "Small": "Klein", + "Middle": "Middel", + "Large": "Groot", + "Size": "Grootte", + "Oversized": "Te groot", + "Auto": "Automatisch", + "Object Fit": "Objectaanpassing", + "Cover": "Bedekken", + "Fill": "Vullen", + "Contain": "Bevatten", + "Scale Down": "Schalen naar beneden", + "Menu item title": "Menutitel", + "Menu item icon": "Menupictogram", + "Target": "Doel", + "Position": "Positie", + "Insert before": "Invoegen voor", + "Insert after": "Invoegen na", + "UI Editor": "UI Bewerker", + "ASC": "Oplopend", + "DESC": "Aflopend", + "Add sort field": "Sorteerveld toevoegen", + "ID": "ID", + "Identifier for program usage. Support letters, numbers and underscores, must start with an letter.": "Identificator voor programma gebruik. Ondersteunt letters, cijfers en underscores, moet met een letter beginnen.", + "Drawer": "Lade", + "Dialog": "Dialoog", + "Delete action": "Actie verwijderen", + "Custom column title": "Aangepaste kolomtitel", + "Column title": "kolomtitel", + "Original title: ": "Originele titel: ", + "Delete table column": "Tabelkolom verwijderen", + "Skip required validation": "Vereiste validatie overslaan", + "Form values": "Formuliervelden", + "Fields values": "Veldenwaarden", + "The field has been deleted": "Het veld is verwijderd", + "When submitting the following fields, the saved values are": "Bij het indienen van de volgende velden worden de waarden opgeslagen als", + "After successful submission": "Na succesvolle indiening", + "Then": "Dan", + "Stay on current page": "Blijf op de huidige pagina", + "Redirect to": "Omleiden naar", + "Save action": "Actie opslaan", + "Exists": "Bestaat", + "Add condition": "Voorwaarde toevoegen", + "Add condition group": "Voorwaardengroep toevoegen", + "exists": "bestaat", + "not exists": "bestaat niet", + "Style": "Stijl", + "=": "=", + "≠": "≠", + ">": ">", + "≥": "≥", + "<": "<", + "≤": "≤", + "Role UID": "Rol-ID", + "Precision": "Precisie", + "Formula mode": "Formulemodus", + "Expression": "Expressie", + "Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Voer +, -, *, /, ( ) in om te berekenen, voer @ in om veldvariabelen te openen.", + "Formula error.": "Formulefout.", + "Rich Text": "Opgemaakte tekst", + "Junction collection": "Koppelingstabel", + "Leave it blank, unless you need a custom intermediate table": "Laat leeg, tenzij je een aangepaste tussenliggende tabel nodig hebt", + "Fields": "Velden", + "Edit field title": "Veldtitel bewerken", + "Field title": "Veldtitel", + "Original field title: ": "Originele veldtitel: ", + "Edit tooltip": "Tooltip bewerken", + "Delete field": "Veld verwijderen", + "Select collection": "Selecteer collectie", + "Blank block": "Leeg blok", + "Duplicate template": "Sjabloon dupliceren", + "Reference template": "Sjabloon refereren", + "Create calendar block": "Kalenderblok maken", + "Create kanban block": "Kanbanblok maken", + "Grouping field": "Groepeer veld", + "Single select and radio fields can be used as the grouping field": "Enkelvoudige selecteer- en keuzerondjevelden kunnen worden gebruikt als groepeerveld", + "Tab name": "Tabbladnaam", + "Current record blocks": "Huidige recordblokken", + "Popup message": "Popupbericht", + "Delete role": "Rol verwijderen", + "Role display name": "Rolweergavenaam", + "Default role": "Standaardrol", + "All collections use general action permissions by default; permission configured individually will override the default one.": "Alle collecties gebruiken standaard algemene actierechten; individueel geconfigureerde rechten overschrijven de standaardinstellingen.", + "Allows configuration of the whole system, including UI, collections, permissions, etc.": "Maakt configuratie van het hele systeem mogelijk, inclusief gebruikersinterface, collecties, rechten, enz.", + "New menu items are allowed to be accessed by default.": "Nieuwe menu-items zijn standaard toegankelijk.", + "Global permissions": "Globale rechten", + "General permissions": "Algemene rechten", + "Global action permissions": "Globale actierechten", + "General action permissions": "Algemene actierechten", + "Plugin settings permissions": "Rechten voor plugin-instellingen", + "Allow to desgin pages": "Toestaan om pagina's te ontwerpen", + "Allow to manage plugins": "Toestaan om plugins te beheren", + "Allow to configure plugins": "Toestaan om plugins te configureren", + "Allows to configure interface": "Toestemming om de interface te configureren", + "Allows to install, activate, disable plugins": "Toestemming om plugins te installeren, activeren of uitschakelen", + "Allows to configure plugins": "Toestemming om plugins te configureren", + "Action display name": "Actieweergavenaam", + "Allow": "Toestaan", + "Data scope": "Databereik", + "Action on new records": "Actie op nieuwe records", + "Action on existing records": "Actie op bestaande records", + "All records": "Alle records", + "Own records": "Eigen records", + "Permission policy": "Rechtenbeleid", + "Individual": "Individueel", + "General": "Algemeen", + "Accessible": "Toegankelijk", + "Configure permission": "Rechten configureren", + "Action permission": "Actierecht", + "Field permission": "Veldrecht", + "Scope name": "Bereiksnaam", + "Unsaved changes": "Niet-opgeslagen wijzigingen", + "Are you sure you don't want to save?": "Weet je zeker dat je niet wil opslaan?", + "Dragging": "Slepen", + "Popup": "Popup", + "Trigger workflow": "Workflow activeren", + "Request API": "API-aanvraag", + "Assign field values": "Veldwaarden toewijzen", + "Constant value": "Constante waarde", + "Dynamic value": "Dynamische waarde", + "Current user": "Huidige gebruiker", + "Current role": "Huidige rol", + "Current record": "Huidig record", + "Current collection": "Huidige collectie", + "Other collections": "Andere collecties", + "Current popup record": "Huidig popup-record", + "Parent popup record": "Ouder-popuprecord", + "Associated records": "Gekoppelde records", + "Parent record": "Hoofdrecord", + "Current time": "Huidige tijd", + "System variables": "Systeemvariabelen", + "Date variables": "Datumvariabelen", + "Message popup close method": "Bericht-popup sluitmethode", + "Automatic close": "Automatisch sluiten", + "Manually close": "Handmatig sluiten", + "After successful update": "Na succesvolle update", + "Save record": "Record opslaan", + "Updated successfully": "Succesvol bijgewerkt", + "After successful save": "Na succesvol opslaan", + "After clicking the custom button, the following field values will be assigned according to the following form.": "Na het klikken op de aangepaste knop worden de volgende veldwaarden toegewezen volgens het onderstaande formulier.", + "After clicking the custom button, the following fields of the current record will be saved according to the following form.": "Na het klikken op de aangepaste knop worden de volgende velden van het huidige record opgeslagen volgens het onderstaande formulier.", + "Button background color": "Knopachtergrondkleur", + "Highlight": "Markeren", + "Danger red": "Waarschuwing rood", + "Custom request": "Aangepaste aanvraag", + "Request settings": "Aanvraaginstellingen", + "Request URL": "Aanvraag-URL", + "Request method": "Aanvraagmethode", + "Request query parameters": "Aanvraagqueryparameters", + "Request headers": "Aanvraagheaders", + "Request body": "Aanvraaginhoud", + "Request success": "Aanvraag geslaagd", + "Invalid JSON format": "Ongeldig JSON-formaat", + "After successful request": "Na succesvolle aanvraag", + "Add exportable field": "Exporteerbaar veld toevoegen", + "Audit logs": "Auditlogboeken", + "Record ID": "Record-ID", + "User": "Gebruiker", + "Field": "Veld", + "Select": "Selecteren", + "Select field": "Veld selecteren", + "Field value changes": "Wijzigingen in veldwaarden", + "One to one (has one)": "Eén op één (heeft één)", + "One to one (belongs to)": "Eén op één (behoort tot)", + "Use the same time zone (GMT) for all users": "Gebruik dezelfde tijdzone (GMT) voor alle gebruikers", + "Province/city/area name": "Provincie/stad/gebiedsnaam", + "Enabled languages": "Ingeschakelde talen", + "View all plugins": "Alle plugins bekijken", + "Print": "Afdrukken", + "Done": "Klaar", + "Sign up successfully, and automatically jump to the sign in page": "Succesvol geregistreerd, springt automatisch naar de inlogpagina", + "File manager": "Bestandsbeheer", + "ACL": "Toegangscontrolelijst", + "Collection manager": "Collectiebeheerder", + "Plugin manager": "Pluginbeheerder", + "Local": "Lokaal", + "Built-in": "Ingebouwd", + "Marketplace": "Marktplaats", + "Add plugin": "Plugin toevoegen", + "Plugin source": "Pluginbron", + "Upgrade": "Upgrade", + "Plugin dependencies check failed": "Controle van plug-in afhankelijkheden mislukt", + "More details": "Meer details", + "Upload new version": "Nieuwe versie uploaden", + "Version": "Versie", + "Npm package": "Npm-pakket", + "Npm package name": "Naam van npm-pakket", + "Upload plugin": "Plugin uploaden", + "Official plugin": "Officiële plugin", + "Add type": "Type toevoegen", + "Changelog": "Wijzigingslogboek", + "Dependencies check": "Afhankelijkhedencontrole", + "Update plugin": "Plugin bijwerken", + "Installing": "Installeren", + "The deletion was successful.": "De verwijdering is succesvol.", + "Plugin Zip File": "Plugin-zipbestand", + "Compressed file url": "URL van gecomprimeerd bestand", + "Last updated": "Laatst bijgewerkt", + "PackageName": "Pakketnaam", + "DisplayName": "Weergavenaam", + "Readme": "Lees mij", + "Dependencies compatibility check": "Compatibiliteitscontrole afhankelijkheden", + "Plugin dependencies check failed, you should change the dependent version to meet the version requirements.": "Controle van plug-in afhankelijkheden mislukt. Je moet de afhankelijke versie aanpassen aan de versievereisten.", + "Version range": "Versiebereik", + "Plugin's version": "Plug-in versie", + "Result": "Resultaat", + "No CHANGELOG.md file": "Geen CHANGELOG.md-bestand", + "No README.md file": "Geen README.md-bestand", + "Homepage": "Homepage", + "Drag and drop the file here or click to upload, file size should not exceed 30M": "Sleep het bestand hierheen of klik om te uploaden, bestandsgrootte mag niet groter zijn dan 30 MB", + "Dependencies check failed, can't enable.": "Controle van afhankelijkheden mislukt, inschakelen niet mogelijk.", + "Plugin starting...": "Plugin wordt gestart...", + "Plugin stopping...": "Plugin wordt gestopt...", + "Are you sure to delete this plugin?": "Weet je zeker dat je deze plugin wil verwijderen?", + "Are you sure to disable this plugin?": "Weet je zeker dat je deze plugin wil uitschakelen?", + "re-download file": "bestand opnieuw downloaden", + "Not enabled": "Niet ingeschakeld", + "Search plugin": "Plugin zoeken", + "Author": "Auteur", + "Plugin loading failed. Please check the server logs.": "Het laden van de plugin is mislukt. Controleer de serverlogs.", + "Coming soon...": "Binnenkort beschikbaar...", + "All plugin settings": "Alle plugininstellingen", + "Bookmark": "Bladwijzer", + "Manage all settings": "Beheer alle instellingen", + "Create inverse field in the target collection": "Creëer een omgekeerd veld in de doelcollectie", + "Inverse field name": "Omgekeerde veldnaam", + "Inverse field display name": "Weergavenaam omgekeerd veld", + "Bulk update": "Updaten in bulk", + "After successful bulk update": "Na succesvolle update in bulk", + "Bulk edit": "Bewerken in bulk", + "Data will be updated": "Gegevens worden bijgewerkt", + "Selected": "Geselecteerd", + "All": "Alles", + "Update selected data?": "Geselecteerde gegevens bijwerken?", + "Update all data?": "Alle gegevens bijwerken?", + "Remains the same": "Blijft hetzelfde", + "Changed to": "Veranderd in", + "Clear": "Wissen", + "Add attach": "Bijlage toevoegen", + "Please select the records to be updated": "Selecteer de records die moeten worden bijgewerkt", + "Selector": "Selector", + "Inner": "Intern", + "Search and select collection": "Zoek en selecteer collectie", + "Please fill in the iframe URL": "Vul de iframe-URL in", + "Fix block": "Blok vastzetten", + "Plugin name": "Pluginnaam", + "Plugin tab name": "Naam plugintabblad", + "AutoGenId": "Automatisch gegenereerd ID veld", + "CreatedBy": "Aangemaakt door", + "UpdatedBy": "Bijgewerkt door", + "CreatedAt": "Aangemaakt op", + "UpdatedAt": "Bijgewerkt op", + "Column width": "Kolombreedte", + "Sortable": "Sorteerbaar", + "Enable link": "Link inschakelen", + "This is likely a NocoBase internals bug. Please open an issue at <1>here": "Dit is waarschijnlijk een interne bug in NocoBase. Open een issue via <1>hier", + "Render Failed": "Renderen mislukt", + "App error": "App-fout", + "Feedback": "Feedback", + "Try again": "Probeer opnieuw", + "Download logs": "Logs downloaden", + "Data template": "Gegevenssjabloon", + "Duplicate": "Dupliceren", + "Duplicating": "Dupliceren", + "Duplicate mode": "Dupliceermodus", + "Quick duplicate": "Snel dupliceren", + "Duplicate and continue": "Dupliceren en doorgaan", + "Please configure the duplicate fields": "Configureer de te dupliceren velden", + "Add": "Toevoegen", + "Add new mode": "Nieuwe modus toevoegen", + "Quick add": "Snel toevoegen", + "Modal add": "Toevoegen via venster", + "Save mode": "Opslagmodus", + "First or create": "Eerste of aanmaken", + "Update or create": "Bijwerken of aanmaken", + "Find by the following fields": "Zoek op de volgende velden", + "Create": "Aanmaken", + "Current form": "Huidig formulier", + "Current object": "Huidig object", + "Linkage with form fields": "Koppeling met formulier velden", + "Allow add new, update and delete actions": "Toestaan om toe te voegen, bijwerken en verwijderen", + "Date display format": "Weergaveformaat datum", + "Assign data scope for the template": "Gegevensbereik toewijzen aan sjabloon", + "Table selected records": "Tabel geselecteerde records", + "Tag": "Label", + "Tag color field": "Labelkleurveld", + "Sync successfully": "Succesvol gesynchroniseerd", + "Sync from form fields": "Synchroniseren vanaf formulier velden", + "Select all": "Alles selecteren", + "Restart": "Herstarten", + "Restart application": "Applicatie herstarten", + "Cascade Select": "Cascade selectie", + "Execute": "Uitvoeren", + "Please use a valid SELECT or WITH AS statement": "Gebruik een geldige SELECT of WITH AS-verklaring", + "Please confirm the SQL statement first": "Bevestig eerst de SQL-verklaring", + "Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "Automatisch objecten verwijderen die afhankelijk zijn van de collectie (zoals weergaven) en ook alle objecten die daarvan afhankelijk zijn", + "Sign in with another account": "Aanmelden met een ander account", + "Return to the main application": "Terug naar de hoofdapplicatie", + "Permission deined": "Toegang geweigerd", + "loading": "laden", + "name is required": "naam is vereist", + "data source": "gegevensbron", + "Data source": "Gegevensbron", + "DataSource": "Gegevensbron", + "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "Het {{type}} \"{{name}}\" is mogelijk verwijderd. Verwijder dit {{blockType}}.", + "Preset fields": "Voorinstellingen", + "Home page": "Startpagina", + "Handbook": "Handleiding", + "License": "Licentie", + "Generic properties": "Algemene eigenschappen", + "Specific properties": "Specifieke eigenschappen", + "Used for drag and drop sorting scenarios, supporting grouping sorting": "Gebruikt voor drag and drop sorteren, met ondersteuning voor groeperen", + "Grouped sorting": "Gegroepeerd sorteren", + "When a field is selected for grouping, it will be grouped first before sorting.": "Als een veld wordt geselecteerd voor groepering, wordt dit eerst gegroepeerd voordat er wordt gesorteerd.", + "Departments": "Afdelingen", + "Main department": "Hoofdafdeling", + "Department name": "Afdelingsnaam", + "Superior department": "Overkoepelende afdeling", + "Owners": "Eigenaren", + "Plugin settings": "Plugin-instellingen", + "Menu": "Menu", + "Drag and drop sorting field": "Sorteren via drag and drop", + "This variable has been deprecated and can be replaced with \"Current form\"": "Deze variabele is verouderd en kan worden vervangen door \"Huidig formulier\"", + "The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "De waarde van deze variabele is afgeleid van de querystring in de URL. Deze variabele werkt alleen correct als de pagina een querystring bevat.", + "URL search params": "URL zoekparameters", + "Expand All": "Alles uitvouwen", + "Search": "Zoeken", + "Clear default value": "Standaardwaarde wissen", + "Open in new window": "Openen in nieuw venster", + "Sorry, the page you visited does not exist.": "Sorry, de pagina die je bezocht bestaat niet.", + "is none of": "is geen van", + "is any of": "is een van", + "Plugin dependency version mismatch": "Versieafhankelijkheid van plugin komt niet overeen", + "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "De huidige afhankelijkheidsversie van de plugin komt niet overeen met de versie van de applicatie en werkt mogelijk niet correct. Weet je zeker dat je de plugin wil blijven inschakelen?", + "Allow multiple selection": "Meerdere selecties toestaan", + "Parent object": "Bovenliggend object", + "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "Sla het ophalen van het totale aantal tabelrecords over tijdens paginering om het laden te versnellen. Het wordt aanbevolen om deze optie in te schakelen voor datatabellen met veel gegevens.", + "Enable secondary confirmation": "Tweede bevestiging inschakelen", + "Notification": "Melding", + "Ellipsis overflow content": "Inhoud afkorten met ellips", + "Hide column": "Kolom verbergen", + "In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "In configuratiemodus wordt de hele kolom transparant. In niet-configuratiemodus wordt de hele kolom verborgen. Zelfs als de hele kolom verborgen is, blijven de geconfigureerde standaardwaarden en andere instellingen van kracht.", + "Unauthenticated. Please sign in to continue.": "Niet geauthenticeerd. Meld je aan om verder te gaan.", + "User not found. Please sign in again to continue.": "Gebruiker niet gevonden. Meld je opnieuw aan om verder te gaan.", + "Your session has expired. Please sign in again.": "Je sessie is verlopen. Meld je opnieuw aan.", + "User password changed, please signin again.": "Gebruikerswachtwoord gewijzigd, meld je opnieuw aan.", + "Desktop routes": "Desktop-routes", + "Route permissions": "Route-machtigingen", + "New routes are allowed to be accessed by default": "Nieuwe routes zijn standaard toegankelijk", + "Route name": "Routenaam", + "Mobile routes": "Mobiele routes", + "Show in menu": "Weergeven in menu", + "Hide in menu": "Verbergen in menu", + "Path": "Pad", + "Type": "Type", + "Access": "Toegang", + "Routes": "Routes", + "Add child route": "Subroute toevoegen", + "Delete routes": "Routes verwijderen", + "Delete route": "Route verwijderen", + "Are you sure you want to hide these routes in menu?": "Weet je zeker dat je deze routes in het menu wil verbergen?", + "Are you sure you want to show these routes in menu?": "Weet je zeker dat je deze routes in het menu wil weergeven?", + "Are you sure you want to hide this menu?": "Weet je zeker dat je dit menu wil verbergen?", + "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Na verbergen wordt dit menu niet meer weergegeven in de menubalk. Om het opnieuw te tonen, moet je naar de routebeheerpagina gaan om het in te stellen.", + "If selected, the page will display Tab pages.": "Indien geselecteerd, worden tabbladen op de pagina weergegeven.", + "If selected, the route will be displayed in the menu.": "Indien geselecteerd, wordt de route weergegeven in het menu.", + "Are you sure you want to hide this tab?": "Weet je zeker dat je dit tabblad wil verbergen?", + "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.": "Na verbergen wordt dit tabblad niet meer weergegeven in de tabbalk. Om het opnieuw te tonen, moet je naar de routebeheerpagina gaan om het in te stellen." + } \ No newline at end of file diff --git a/packages/core/client/src/locale/pt-BR.json b/packages/core/client/src/locale/pt-BR.json index 83211216fa..40a42c7b42 100644 --- a/packages/core/client/src/locale/pt-BR.json +++ b/packages/core/client/src/locale/pt-BR.json @@ -778,6 +778,9 @@ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Depois de ocultar, este menu não aparecerá mais na barra de menus. Para mostrar novamente, você precisa ir à página de gerenciamento de rotas para configurá-lo.", "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.", "Deprecated": "Descontinuado", - "The following old template features have been deprecated and will be removed in next version.": "As seguintes funcionalidades de modelo antigo foram descontinuadas e serão removidas na próxima versão." + "The following old template features have been deprecated and will be removed in next version.": "As seguintes funcionalidades de modelo antigo foram descontinuadas e serão removidas na próxima versão.", + "Full permissions": "Todas as permissões" } diff --git a/packages/core/client/src/locale/ru-RU.json b/packages/core/client/src/locale/ru-RU.json index 01750eff7c..1ed4729523 100644 --- a/packages/core/client/src/locale/ru-RU.json +++ b/packages/core/client/src/locale/ru-RU.json @@ -607,6 +607,9 @@ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "После скрытия этого меню он больше не будет отображаться в меню. Чтобы снова отобразить его, вам нужно будет перейти на страницу управления маршрутами и настроить его.", "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.": "После скрытия этой вкладки она больше не будет отображаться во вкладке. Чтобы снова отобразить ее, вам нужно будет перейти на страницу управления маршрутами, чтобы установить ее.", "Deprecated": "Устаревший", - "The following old template features have been deprecated and will be removed in next version.": "Следующие старые функции шаблонов устарели и будут удалены в следующей версии." + "The following old template features have been deprecated and will be removed in next version.": "Следующие старые функции шаблонов устарели и будут удалены в следующей версии.", + "Full permissions": "Полные права" } diff --git a/packages/core/client/src/locale/tr-TR.json b/packages/core/client/src/locale/tr-TR.json index 2532479a2f..adb9a74315 100644 --- a/packages/core/client/src/locale/tr-TR.json +++ b/packages/core/client/src/locale/tr-TR.json @@ -605,6 +605,9 @@ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Gizlendikten sonra, bu menü artık menü çubuğunda görünmeyecektir. Tekrar görüntülemek için, yönlendirme yönetimi sayfasına gidip onu yapılandırmanız gerekecektir.", "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.", "Deprecated": "Kullanımdan kaldırıldı", - "The following old template features have been deprecated and will be removed in next version.": "Aşağıdaki eski şablon özellikleri kullanımdan kaldırıldı ve gelecek sürümlerde kaldırılacaktır." + "The following old template features have been deprecated and will be removed in next version.": "Aşağıdaki eski şablon özellikleri kullanımdan kaldırıldı ve gelecek sürümlerde kaldırılacaktır.", + "Full permissions": "Tüm izinler" } diff --git a/packages/core/client/src/locale/uk-UA.json b/packages/core/client/src/locale/uk-UA.json index 040e64841f..7100c2e629 100644 --- a/packages/core/client/src/locale/uk-UA.json +++ b/packages/core/client/src/locale/uk-UA.json @@ -821,6 +821,9 @@ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Після приховування цього меню він більше не з'явиться в меню. Щоб знову показати його, вам потрібно перейти на сторінку керування маршрутами і налаштувати його.", "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.": "Після приховування цієї вкладки вона більше не з'явиться в панелі вкладок. Щоб знову показати її, вам потрібно перейти на сторінку керування маршрутами, щоб налаштувати її.", "Deprecated": "Застаріло", - "The following old template features have been deprecated and will be removed in next version.": "Наступні старі функції шаблонів були застарілі і будуть видалені в наступній версії." -} + "The following old template features have been deprecated and will be removed in next version.": "Наступні старі функції шаблонів були застарілі і будуть видалені в наступній версії.", + "Full permissions": "Повні права" +} \ No newline at end of file diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index 8b5e696d4d..db0ad0da99 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -1033,6 +1033,8 @@ "Left": "左", "Center": "居中", "Right": "右", + "Input value": "输入值", + "Output value": "输出值", "Divider line color": "分割线颜色", "Label align": "字段标题对齐方式", "Label width": "字段标题宽度", @@ -1080,7 +1082,13 @@ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "隐藏后,这个菜单将不再出现在菜单栏中。要再次显示它,你需要到路由管理页面进行设置。", "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.": "隐藏后,该标签将不再显示在标签栏中。要想再次显示它,你需要到路由管理页面进行设置。", "Deprecated": "已弃用", - "The following old templates have been deprecated and will be removed in next version.": "以下旧的模板功能已弃用,将在下个版本移除。", - "Array": "数组" + "The following old template features have been deprecated and will be removed in next version.": "以下旧的模板功能已弃用,将在下个版本移除。", + "Full permissions": "全部权限", + "Enable index column": "启用序号列", + "Array": "数组", + "Date scope": "日期范围", + "Icon only": "仅显示图标" } diff --git a/packages/core/client/src/locale/zh-TW.json b/packages/core/client/src/locale/zh-TW.json index fe64fe55c6..1a6607e532 100644 --- a/packages/core/client/src/locale/zh-TW.json +++ b/packages/core/client/src/locale/zh-TW.json @@ -912,7 +912,9 @@ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "隱藏後,這個菜單將不再出現在菜單欄中。要再次顯示它,你需要到路由管理頁面進行設置。", "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.": "隱藏後,這個標籤將不再出現在標籤欄中。要再次顯示它,你需要到路由管理頁面進行設置。", "Deprecated": "已棄用", - "The following old template features have been deprecated and will be removed in next version.": "以下舊的模板功能已棄用,將在下個版本移除。" -} - + "The following old template features have been deprecated and will be removed in next version.": "以下舊的模板功能已棄用,將在下個版本移除。", + "Full permissions": "完全權限" +} \ No newline at end of file diff --git a/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts b/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts index 777b9bb5c1..3a45772323 100644 --- a/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts +++ b/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts @@ -28,7 +28,7 @@ test.describe('bulk-destroy', () => { // 3. 点击批量删除按钮,Table 显示无数据 await page.getByLabel('action-Action-Delete-destroy-').click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); - await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data').last()).toBeVisible(); }); test('Secondary confirmation', async ({ page, mockPage, mockRecords }) => { @@ -45,6 +45,7 @@ test.describe('bulk-destroy', () => { await page.getByLabel('designer-schema-settings-Action-actionSettings:bulkDelete-general').hover(); await page.getByRole('menuitem', { name: 'Secondary confirmation' }).click(); await page.getByLabel('Enable secondary confirmation').uncheck(); + await expect(page.getByRole('button', { name: 'OK' })).toHaveCount(1); await page.getByRole('button', { name: 'OK' }).click(); await page.mouse.move(500, 0); @@ -53,6 +54,6 @@ test.describe('bulk-destroy', () => { // 3. 点击批量删除按钮,Table 显示无数据 await page.getByLabel('action-Action-Delete-destroy-').click(); - await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data').last()).toBeVisible(); }); }); diff --git a/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts b/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts index ca11ae838b..8b2a31eddb 100644 --- a/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts +++ b/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts @@ -28,6 +28,9 @@ test.describe('Link', () => { // 2. config the Link button await page.getByLabel('action-Action.Link-Link-customize:link-users-table-0').hover(); + await expect( + page.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:link-users' }), + ).toHaveCount(1); await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:link-users' }).hover(); await page.getByRole('menuitem', { name: 'Edit link' }).click(); await page diff --git a/packages/core/client/src/modules/actions/__e2e__/link/templates.ts b/packages/core/client/src/modules/actions/__e2e__/link/templates.ts index e9578e32ad..1e5af6a0fc 100644 --- a/packages/core/client/src/modules/actions/__e2e__/link/templates.ts +++ b/packages/core/client/src/modules/actions/__e2e__/link/templates.ts @@ -884,7 +884,7 @@ export const URLSearchParamsUseAssociationFieldValue = { linkageAction: true, }, 'x-component-props': { - url: '/admin/ids0d9esx8k', + url: '/admin/ocal3pnltf2', params: [ { name: 'roles', diff --git a/packages/core/client/src/modules/actions/associate/__e2e__/associate.test.ts b/packages/core/client/src/modules/actions/associate/__e2e__/associate.test.ts index 5b22c812ff..6274bd3f37 100644 --- a/packages/core/client/src/modules/actions/associate/__e2e__/associate.test.ts +++ b/packages/core/client/src/modules/actions/associate/__e2e__/associate.test.ts @@ -29,6 +29,7 @@ test('basic', async ({ page, mockPage, mockRecord }) => { await expect(page.getByRole('tooltip').getByText('Disassociate')).toBeVisible(); await page.getByLabel('block-item-CardItem-cc-table').hover(); + await page.getByRole('menuitem', { name: 'Associate' }).waitFor({ state: 'detached' }); await page.getByLabel('schema-initializer-ActionBar-table:configureActions-cc').hover(); await page.getByRole('menuitem', { name: 'Associate' }).click(); //点击 associate 出现弹窗 diff --git a/packages/core/client/src/modules/actions/disassociate/__e2e__/disassociate.test.ts b/packages/core/client/src/modules/actions/disassociate/__e2e__/disassociate.test.ts index f9e2832325..ee224d48f6 100644 --- a/packages/core/client/src/modules/actions/disassociate/__e2e__/disassociate.test.ts +++ b/packages/core/client/src/modules/actions/disassociate/__e2e__/disassociate.test.ts @@ -18,7 +18,7 @@ test('basic', async ({ page, mockPage, mockRecord }) => { await page.getByLabel('action-Action.Link-Edit record-update-collection1-table-0').click(); await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover(); - await page.getByRole('menuitem', { name: 'Associated records' }).hover(); + await page.getByRole('menuitem', { name: 'Associated records' }).last().hover(); await page.getByRole('menuitem', { name: 'manyToMany' }).click(); // 2. Table 中显示 Role UID 字段 diff --git a/packages/core/client/src/modules/blocks/BlockSchemaToolbar.tsx b/packages/core/client/src/modules/blocks/BlockSchemaToolbar.tsx index 30dfacee5a..6e019f64ab 100644 --- a/packages/core/client/src/modules/blocks/BlockSchemaToolbar.tsx +++ b/packages/core/client/src/modules/blocks/BlockSchemaToolbar.tsx @@ -14,6 +14,7 @@ import { useCollection } from '../../data-source/collection/CollectionProvider'; import { useCompile } from '../../schema-component'; import { SchemaToolbar } from '../../schema-settings/GeneralSchemaDesigner'; import { useSchemaTemplate } from '../../schema-templates'; +import { useMobileLayout } from '../../route-switch/antd/admin-layout'; export const BlockSchemaToolbar = (props) => { const { t } = useTranslation(); @@ -22,6 +23,7 @@ export const BlockSchemaToolbar = (props) => { const template = useSchemaTemplate(); const { association, collection } = useDataBlockProps() || {}; const compile = useCompile(); + const { isMobileLayout } = useMobileLayout(); if (association) { const [collectionName] = association.split('.'); @@ -51,7 +53,7 @@ export const BlockSchemaToolbar = (props) => { ].filter(Boolean); }, [currentCollectionTitle, currentCollectionName, associationField, associationCollection, compile, templateName]); - return ; + return ; }; export function getCollectionTitle(arg: { diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts index 42f32a51c8..54684025a7 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts @@ -12,6 +12,7 @@ import { oneEmptyTableWithUsers } from './templatesOfBug'; const deleteButton = async (page: Page, name: string) => { await page.getByRole('button', { name }).hover(); + await page.getByRole('menuitem', { name: 'Delete' }).waitFor({ state: 'detached' }); await page.getByRole('button', { name }).getByLabel('designer-schema-settings-').hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); @@ -31,6 +32,7 @@ test.describe('where multi data details block can be added', () => { // 1. 打开弹窗,通过 Associated records 添加一个详情区块 await page.getByLabel('action-Action.Link-View').click(); await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'Associated records right' }).waitFor({ state: 'detached' }); await page.getByRole('menuitem', { name: 'Details right' }).hover(); await page.getByRole('menuitem', { name: 'Associated records right' }).hover(); await page.getByRole('menuitem', { name: 'Roles' }).click(); @@ -41,6 +43,7 @@ test.describe('where multi data details block can be added', () => { await expect(page.getByLabel('block-item-CollectionField-').getByText('admin')).toBeVisible(); // 2. 打开弹窗,通过 Other records 添加一个详情区块 + await page.getByRole('menuitem', { name: 'Details right' }).waitFor({ state: 'detached' }); await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByRole('menuitem', { name: 'Details right' }).hover(); await page.getByRole('menuitem', { name: 'Other records right' }).hover(); @@ -116,6 +119,7 @@ test.describe('configure actions', () => { await page.getByText('Delete').click(); await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Edit' })).toHaveCount(1); await expect(page.getByRole('button', { name: 'Edit' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaSettings.test.ts index 23faffa28b..4168cb2752 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaSettings.test.ts @@ -76,6 +76,7 @@ test.describe('actions schema settings', () => { await expectSettingsMenu({ page, showMenu: async () => { + await expect(page.getByRole('button', { name: 'Edit' })).toHaveCount(1); await page.getByRole('button', { name: 'Edit' }).hover(); await page.getByRole('button', { name: 'designer-schema-settings-Action' }).hover(); }, diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/setDataLoadingModeSettingsItem.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/setDataLoadingModeSettingsItem.test.ts index eaa98da272..3402c78d39 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/setDataLoadingModeSettingsItem.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/setDataLoadingModeSettingsItem.test.ts @@ -50,10 +50,10 @@ test.describe('setDataLoadingModeSettingsItem', () => { await page.getByRole('button', { name: 'OK', exact: true }).click(); // 所有区块应该显示 No data - await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible(); - await expect(page.getByLabel('block-item-CardItem-users-details').getByText('No data')).toBeVisible(); - await expect(page.getByLabel('block-item-CardItem-users-list').getByText('No data')).toBeVisible(); - await expect(page.getByLabel('block-item-BlockItem-users-').getByText('No data')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-users-details').getByText('No data').last()).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-users-list').getByText('No data').last()).toBeVisible(); + await expect(page.getByLabel('block-item-BlockItem-users-').getByText('No data').last()).toBeVisible(); // 3. 在筛选表单中数据一个筛选条件,点击筛选按钮,区块内应该显示数据 await page.getByLabel('block-item-CollectionField-').getByRole('textbox').click(); @@ -67,10 +67,10 @@ test.describe('setDataLoadingModeSettingsItem', () => { // 4. 点击筛选表单的 Reset 按钮,区块内应该显示 No data await page.getByLabel('action-Action-Reset to empty-users-').click(); - await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible(); - await expect(page.getByLabel('block-item-CardItem-users-details').getByText('No data')).toBeVisible(); - await expect(page.getByLabel('block-item-CardItem-users-list').getByText('No data')).toBeVisible(); - await expect(page.getByLabel('block-item-BlockItem-users-').getByText('No data')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-users-details').getByText('No data').last()).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-users-list').getByText('No data').last()).toBeVisible(); + await expect(page.getByLabel('block-item-BlockItem-users-').getByText('No data').last()).toBeVisible(); }); test('When the data block has data scope settings and dataLoadingMode is manual, data should not be displayed after the first page load', async ({ @@ -78,7 +78,7 @@ test.describe('setDataLoadingModeSettingsItem', () => { mockPage, }) => { await mockPage(TableBlockWithDataScope).goto(); - await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible(); // 此时点击 filter 按钮,应该还是没数据,因为表单没有值 await page.getByLabel('action-Action-Filter-submit-').click({ @@ -87,7 +87,7 @@ test.describe('setDataLoadingModeSettingsItem', () => { y: 10, }, }); - await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible(); // 点击 Reset 按钮,也是一样 await page.getByLabel('action-Action-Reset-users-').click({ @@ -96,6 +96,6 @@ test.describe('setDataLoadingModeSettingsItem', () => { y: 10, }, }); - await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible(); }); }); diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts index ceb518239b..97c246cfcc 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts @@ -69,7 +69,7 @@ test.describe('where single data details block can be added', () => { // 3.通过 Associated records 创建一个详情区块 await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByRole('menuitem', { name: 'Details right' }).hover(); - await page.getByRole('menuitem', { name: 'Associated records' }).hover(); + await page.getByRole('menuitem', { name: 'Associated records' }).last().hover(); await page.getByRole('menuitem', { name: 'manyToOne' }).hover(); await page.getByRole('menuitem', { name: 'Blank block' }).click(); await page.mouse.move(300, 0); @@ -82,7 +82,7 @@ test.describe('where single data details block can be added', () => { // 4.通过 Associated records 创建一个详情区块,使用模板 await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByRole('menuitem', { name: 'Details right' }).hover(); - await page.getByRole('menuitem', { name: 'Associated records' }).hover(); + await page.getByRole('menuitem', { name: 'Associated records' }).last().hover(); await page.getByRole('menuitem', { name: 'manyToOne' }).hover(); await page.getByRole('menuitem', { name: 'Duplicate template' }).hover(); await page.getByRole('menuitem', { name: 'example_Details (Fields only)' }).click(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts index 9eec7020d5..c806156f37 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts @@ -60,10 +60,10 @@ test.describe('configure fields', () => { await expect(page.getByRole('menuitem', { name: 'ID', exact: true }).getByRole('switch')).toBeChecked(); // add association fields - await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Many to one right' }).hover(); await page.getByRole('menuitem', { name: 'Nickname' }).click(); - await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Many to one right' }).hover(); await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).toBeChecked(); await page.mouse.move(300, 0); @@ -72,13 +72,14 @@ test.describe('configure fields', () => { // delete fields await page.getByLabel('schema-initializer-Grid-form:configureFields-general').hover(); + await expect(page.getByRole('menuitem', { name: 'ID', exact: true })).toHaveCount(1); await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); await expect(page.getByRole('menuitem', { name: 'ID', exact: true }).getByRole('switch')).not.toBeChecked(); - await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Many to one right' }).hover(); await page.getByRole('menuitem', { name: 'Nickname' }).click(); - await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Many to one right' }).hover(); await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).not.toBeChecked(); await page.mouse.move(300, 0); @@ -119,6 +120,7 @@ test.describe('configure actions', () => { // add button await page.getByRole('menuitem', { name: 'Submit' }).click(); await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Submit' })).toHaveCount(1); await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); // delete button diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts index e210a3172e..86eda66087 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts @@ -279,7 +279,7 @@ test.describe('set default value', () => { await page.getByRole('button', { name: 'OK', exact: true }).click(); // 2. 设置的 ‘abcd’ 应该立即显示在 Nickname 字段的输入框中 - await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abcd'); + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox').last()).toHaveValue('abcd'); }); test('Current popup record', async ({ page, mockPage }) => { diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings1.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings1.test.ts index 99965cbaab..31e53ebdce 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings1.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings1.test.ts @@ -248,7 +248,7 @@ test.describe('creation form block schema settings', () => { // 重新选择一下数据,字段值才会被填充 // TODO: 保存后,数据应该直接被填充上 - await page.getByLabel('icon-close-select').click(); + await page.getByLabel('icon-close-select').last().click(); await page.getByTestId('select-object-single').click(); await page.getByRole('option', { name: '2' }).click(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts index 1f9cdfa55b..a3a6a6290e 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts @@ -128,9 +128,14 @@ test.describe('linkage rules', () => { // 增加一条规则:当 number 字段的值等于 123 时 await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); - await page.locator('.ant-collapse-header').nth(1).getByRole('img', { name: 'right' }).click(); + await page.locator('.ant-collapse-header .ant-collapse-expand-icon').nth(1).click(); - await page.getByLabel('Linkage rules').getByRole('tabpanel').getByText('Add condition', { exact: true }).click(); + await page + .getByLabel('Linkage rules') + .getByRole('tabpanel') + .getByText('Add condition', { exact: true }) + .last() + .click(); await page.getByRole('button', { name: 'Select field' }).click(); await page.getByRole('menuitemcheckbox', { name: 'number' }).click(); await page.getByLabel('Linkage rules').getByRole('spinbutton').click(); @@ -146,19 +151,19 @@ test.describe('linkage rules', () => { // action: 为 longText 字段赋上常量值 await page.getByLabel('Linkage rules').getByRole('tabpanel').getByText('Add property').click(); await page.getByRole('button', { name: 'Select field' }).click(); - await page.getByRole('tree').getByText('longText').click(); + await page.getByRole('tree').getByText('longText').last().click(); await page.getByRole('button', { name: 'action', exact: true }).click(); - await page.getByRole('option', { name: 'Value', exact: true }).click(); + await page.getByRole('option', { name: 'Value', exact: true }).last().click(); await page.getByLabel('dynamic-component-linkage-rules').getByRole('textbox').fill('456'); // action: 为 integer 字段附上一个表达式,使其值等于 number 字段的值 await page.getByLabel('Linkage rules').getByRole('tabpanel').getByText('Add property').click(); await page.getByRole('button', { name: 'Select field' }).click(); - await page.getByRole('tree').getByText('integer').click(); + await page.getByRole('tree').getByText('integer').last().click(); await page.getByRole('button', { name: 'action', exact: true }).click(); - await page.getByRole('option', { name: 'Value', exact: true }).click(); - await page.getByTestId('select-linkage-value-type').nth(1).click(); + await page.getByRole('option', { name: 'Value', exact: true }).last().click(); + await page.getByTestId('select-linkage-value-type').last().click(); await page.getByText('Expression').click(); await page.getByText('xSelect a variable').click(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/bulkEditForm.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/bulkEditForm.test.ts index d4d5c94351..5dcdb7979d 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/bulkEditForm.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/bulkEditForm.test.ts @@ -33,6 +33,7 @@ test.describe('bulk edit form', () => { await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeVisible(); // 3. 输入值,点击提交 + await expect(page.getByLabel('block-item-BulkEditField-').getByRole('textbox')).toHaveCount(1); await page.getByLabel('block-item-BulkEditField-').getByRole('textbox').fill('123'); await page.getByRole('button', { name: 'Submit' }).click(); @@ -65,6 +66,7 @@ test.describe('bulk edit form', () => { await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeVisible(); // 4. 点击提交按钮,应该提示一个错误 + await expect(page.getByRole('button', { name: 'Submit' })).toHaveCount(1); await page.getByRole('button', { name: 'Submit' }).click(); await expect(page.getByLabel('block-item-BulkEditField-').getByText('The field value is required')).toBeVisible(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/deprecatedVariables.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/deprecatedVariables.test.ts index 8e27ae035c..25c80340d2 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/deprecatedVariables.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/deprecatedVariables.test.ts @@ -34,6 +34,7 @@ test.describe('deprecated variables', () => { // 表达式输入框也是一样 await page.getByText('xSelect a variable').click(); + await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveCount(1); await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).hover({ position: { x: 40, y: 12 } }); await expect(page.getByRole('tooltip', { name: 'This variable has been deprecated' })).toBeVisible(); await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveClass( @@ -45,6 +46,7 @@ test.describe('deprecated variables', () => { // 3. 当设置为其它变量后,再次打开,变量列表中的弃用变量不再显示 await page.locator('button').filter({ hasText: /^x$/ }).click(); + await expect(page.getByRole('menuitemcheckbox', { name: 'Current form right' })).toHaveCount(1); await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname')).toBeVisible(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts index 787d818657..48a0412023 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts @@ -18,6 +18,7 @@ import { import { T3825 } from './templatesOfBug'; const clickOption = async (page: Page, optionName: string) => { await page.getByLabel('block-item-CardItem-general-form').hover(); + await page.getByRole('menuitem', { name: optionName }).waitFor({ state: 'detached' }); await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); await page.getByRole('menuitem', { name: optionName }).click(); }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts index 5582f3b32e..506094cbc0 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts @@ -13,6 +13,7 @@ import { oneGridCardWithInheritFields } from './templatesOfBug'; const deleteButton = async (page: Page, name: string) => { await page.getByRole('button', { name }).hover(); + await page.getByRole('menuitem', { name: 'Delete' }).waitFor({ state: 'detached' }); await page.getByRole('button', { name }).getByLabel('designer-schema-settings-').hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); @@ -47,6 +48,7 @@ test.describe('where grid card block can be added', () => { // 2. 通过 Other records 创建一个列表区块 await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'Other records right' }).waitFor({ state: 'detached' }); await page.getByRole('menuitem', { name: 'Grid Card right' }).hover(); await page.getByRole('menuitem', { name: 'Other records right' }).hover(); await page.getByRole('menuitem', { name: 'Users' }).click(); @@ -151,10 +153,10 @@ test.describe('configure fields', () => { // add association fields await page.mouse.wheel(0, 300); - await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Many to one right' }).hover(); await page.getByRole('menuitem', { name: 'Nickname' }).click(); - await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Many to one right' }).hover(); await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).toBeChecked(); await page.mouse.move(300, 0); @@ -165,14 +167,14 @@ test.describe('configure fields', () => { // delete fields await formItemInitializer.hover(); - await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); - await expect(page.getByRole('menuitem', { name: 'ID', exact: true }).getByRole('switch')).not.toBeChecked(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).first().click(); + await expect(page.getByRole('menuitem', { name: 'ID', exact: true }).getByRole('switch').first()).not.toBeChecked(); await page.mouse.wheel(0, 300); - await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Many to one right' }).hover(); await page.getByRole('menuitem', { name: 'Nickname' }).click(); - await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Many to one right' }).hover(); await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).not.toBeChecked(); await page.mouse.move(300, 0); @@ -185,7 +187,7 @@ test.describe('configure fields', () => { // add markdown await formItemInitializer.hover(); - await page.getByRole('menuitem', { name: 'ID', exact: true }).hover(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).first().hover(); await page.mouse.wheel(0, 300); await page.getByRole('menuitem', { name: 'Add Markdown' }).click(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaSettings.test.ts index fec8fa7ba9..cf4c79f1cb 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaSettings.test.ts @@ -18,6 +18,7 @@ test.describe('grid card block schema settings', () => { page, showMenu: async () => { await page.getByLabel('block-item-BlockItem-general-grid-card').hover(); + await page.waitForTimeout(1000); await page.getByLabel('designer-schema-settings-BlockItem-GridCard.Designer-general').hover(); }, supportedOptions: [ diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts index 3c7db0668b..c9b0519b6b 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts @@ -12,6 +12,7 @@ import { oneEmptyTableWithUsers } from '../../details-multi/__e2e__/templatesOfB const deleteButton = async (page: Page, name: string) => { await page.getByRole('button', { name }).hover(); + await page.getByRole('menuitem', { name: 'Delete' }).waitFor({ state: 'detached' }); await page.getByRole('button', { name }).getByLabel('designer-schema-settings-').hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); @@ -71,6 +72,9 @@ test.describe('configure global actions', () => { await page.getByRole('menuitem', { name: 'Refresh' }).click(); await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Filter' })).toHaveCount(1); + await expect(page.getByRole('button', { name: 'Add new' })).toHaveCount(1); + await expect(page.getByRole('button', { name: 'Refresh' })).toHaveCount(1); await expect(page.getByRole('button', { name: 'Filter' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Add new' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Refresh' })).toBeVisible(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts index 4da6804303..6cccd9cfde 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts @@ -90,7 +90,7 @@ test.describe('configure actions column', () => { // 列宽度默认为 100 await expectActionsColumnWidth(100); - await page.getByText('Actions', { exact: true }).hover(); + await page.getByText('Actions', { exact: true }).hover({ force: true }); await page.getByLabel('designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-users').hover(); await page.getByRole('menuitem', { name: 'Column width' }).click(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/hideColumn.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/hideColumn.test.ts index 8a14b3db12..2dded11a76 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/hideColumn.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/hideColumn.test.ts @@ -25,6 +25,7 @@ test.describe('hide column', () => { // 2. Sub table: hide column await page.getByRole('button', { name: 'Role name' }).hover(); + await page.getByRole('menuitem', { name: 'Hide column question-circle' }).waitFor({ state: 'detached' }); await page .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-roles' }) .hover(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts index 669ca57bab..bcabdba6a9 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts @@ -295,17 +295,20 @@ test.describe('configure actions column', () => { await nocoPage.goto(); // add view & Edit & Delete & Duplicate ------------------------------------------------------------ - await page.getByText('Actions', { exact: true }).hover(); + await page.getByText('Actions', { exact: true }).hover({ force: true }); await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); await page.getByRole('menuitem', { name: 'View' }).click(); - await page.getByText('Actions', { exact: true }).hover(); + await page.getByText('Actions', { exact: true }).hover({ force: true }); await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); await page.getByRole('menuitem', { name: 'Edit' }).click(); - await page.getByText('Actions', { exact: true }).hover(); + await page.getByText('Actions', { exact: true }).hover({ force: true }); await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); + + await page.getByText('Actions', { exact: true }).hover({ force: true }); + await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); await page.getByRole('menuitem', { name: 'Duplicate' }).click(); await page.mouse.move(300, 0); @@ -327,11 +330,11 @@ test.describe('configure actions column', () => { await expect(page.getByLabel('action-Action.Link-Duplicate-duplicate-t_unp4scqamw9-table-0')).not.toBeVisible(); // add custom action ------------------------------------------------------------ - await page.getByText('Actions', { exact: true }).hover(); + await page.getByText('Actions', { exact: true }).hover({ force: true }); await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); await page.getByRole('menuitem', { name: 'Popup' }).click(); - await page.getByText('Actions', { exact: true }).hover(); + await page.getByText('Actions', { exact: true }).hover({ force: true }); await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); await page.getByRole('menuitem', { name: 'Update record' }).click(); @@ -348,7 +351,7 @@ test.describe('configure actions column', () => { // 列宽度默认为 100 await expect(page.getByRole('columnheader', { name: 'Actions', exact: true })).toHaveJSProperty('offsetWidth', 100); - await page.getByText('Actions', { exact: true }).hover(); + await page.getByText('Actions', { exact: true }).hover({ force: true }); await page.getByLabel('designer-schema-settings-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); await page.getByRole('menuitem', { name: 'Column width' }).click(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer1.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer1.test.ts index 05e7877d27..7975f16665 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer1.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer1.test.ts @@ -12,6 +12,7 @@ import { T3686, T4005 } from './templatesOfBug'; const deleteButton = async (page: Page, name: string) => { await page.getByRole('button', { name }).hover(); + await page.getByRole('menuitem', { name: 'Delete' }).waitFor({ state: 'detached' }); await page.getByRole('button', { name }).getByLabel('designer-schema-settings-').hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); @@ -37,7 +38,8 @@ test.describe('where table block can be added', () => { // 添加当前表关系区块 await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover(); - await page.getByRole('menuitem', { name: 'Associated records' }).hover(); + await page.getByRole('menuitem', { name: 'childAssociationField' }).waitFor({ state: 'detached' }); + await page.getByRole('menuitem', { name: 'Associated records' }).last().hover(); await page.getByRole('menuitem', { name: 'childAssociationField' }).click(); await page .getByTestId('drawer-Action.Container-childCollection-View record') @@ -46,9 +48,10 @@ test.describe('where table block can be added', () => { await page.getByRole('menuitem', { name: 'childTargetText' }).click(); // 添加父表关系区块 + await page.getByRole('menuitem', { name: 'Table right' }).waitFor({ state: 'detached' }); await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover(); - await page.getByRole('menuitem', { name: 'Associated records' }).hover(); + await page.getByRole('menuitem', { name: 'Associated records' }).last().hover(); await page.getByRole('menuitem', { name: 'parentAssociationField' }).click(); await page.getByLabel('schema-initializer-TableV2-table:configureColumns-parentTargetCollection').hover(); await page.getByRole('menuitem', { name: 'parentTargetText' }).click(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts index 226d3a271b..f51e03f878 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts @@ -87,7 +87,7 @@ test.describe('actions schema settings', () => { // 切换为 dialog await page.getByRole('menuitem', { name: 'Open mode' }).click(); - await page.getByRole('option', { name: 'Dialog' }).click(); + await page.getByRole('option', { name: 'Dialog' }).last().click(); await page.getByRole('button', { name: 'Add new' }).click(); await expect(page.getByTestId('modal-Action.Container-general-Add record')).toBeVisible(); @@ -97,7 +97,7 @@ test.describe('actions schema settings', () => { await page.getByLabel('action-Action-Add new-create-').hover(); await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover(); await page.getByRole('menuitem', { name: 'Open mode Dialog' }).click(); - await page.getByRole('option', { name: 'Page' }).click(); + await page.getByRole('option', { name: 'Page' }).last().click(); // 点击按钮后会跳转到一个页面 await page.getByLabel('action-Action-Add new-create-').click(); @@ -116,7 +116,7 @@ test.describe('actions schema settings', () => { // 创建一条数据后返回,列表中应该有这条数据 await page.getByTestId('select-single').click(); - await page.getByRole('option', { name: 'option3' }).click(); + await page.getByRole('option', { name: 'option3' }).last().click(); // 提交后会自动返回 await page.getByLabel('action-Action-Submit-submit-').click(); @@ -136,7 +136,7 @@ test.describe('actions schema settings', () => { // 切换为 small await page.getByRole('menuitem', { name: 'Popup size' }).click(); - await page.getByRole('option', { name: 'Small' }).click(); + await page.getByRole('option', { name: 'Small' }).last().click(); await page.getByRole('button', { name: 'Add new' }).click(); const drawerWidth = @@ -148,7 +148,7 @@ test.describe('actions schema settings', () => { // 切换为 large await showMenu(page); await page.getByRole('menuitem', { name: 'Popup size' }).click(); - await page.getByRole('option', { name: 'Large' }).click(); + await page.getByRole('option', { name: 'Large' }).last().click(); await page.getByRole('button', { name: 'Add new' }).click(); const drawerWidth2 = @@ -325,7 +325,7 @@ test.describe('actions schema settings', () => { await page.getByText('Add property').click(); await page.getByLabel('block-item-ArrayCollapse-general').click(); await page.getByTestId('select-linkage-properties').click(); - await page.getByRole('option', { name: 'Disabled' }).click(); + await page.getByRole('option', { name: 'Disabled' }).last().click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); await expect(page.getByLabel('action-Action.Link-View record-view-general-table-0')).toHaveAttribute( @@ -336,10 +336,10 @@ test.describe('actions schema settings', () => { // 设置第二组规则 -------------------------------------------------------------------------- await openLinkageRules(); await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); - await page.locator('.ant-collapse-header').nth(1).getByRole('img', { name: 'right' }).click(); + await page.locator('.ant-collapse-header .ant-collapse-expand-icon').nth(1).click(); // 添加一个条件:ID 等于 1 - await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).click(); + await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).last().click(); await page.getByRole('button', { name: 'Select field' }).click(); await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); await page.getByRole('spinbutton').click(); @@ -348,7 +348,7 @@ test.describe('actions schema settings', () => { // action: 使按钮可用 await page.getByRole('tabpanel').getByText('Add property').click(); await page.locator('.ant-select', { hasText: 'action' }).click(); - await page.getByRole('option', { name: 'Enabled' }).click(); + await page.getByRole('option', { name: 'Enabled' }).last().click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); // 后面的 action 会覆盖前面的 @@ -533,7 +533,7 @@ test.describe('actions schema settings', () => { await page.getByLabel('action-Action.Link-View').hover(); await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover(); await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click(); - await page.getByRole('option', { name: 'Page' }).click(); + await page.getByRole('option', { name: 'Page' }).last().click(); // 跳转到子页面后,其内容应该和弹窗中的内容一致 await page.getByLabel('action-Action.Link-View').click(); @@ -706,7 +706,7 @@ test.describe('actions schema settings', () => { .getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:view-roles' }) .hover(); await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click(); - await page.getByRole('option', { name: 'Page' }).click(); + await page.getByRole('option', { name: 'Page' }).last().click(); // 点击按钮跳转到子页面 await page.getByLabel('action-Action.Link-View role-view-roles-table-admin').click(); @@ -955,7 +955,7 @@ test.describe('actions schema settings', () => { await page.getByLabel('action-Action.Link-Add child-').hover(); await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:addChild-treeCollection').hover(); await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click(); - await page.getByRole('option', { name: 'Page' }).click(); + await page.getByRole('option', { name: 'Page' }).last().click(); // open popup with page mode await page.getByLabel('action-Action.Link-Add child-').click(); @@ -994,7 +994,7 @@ test.describe('table column schema settings', () => { // 1. 关系字段下拉框中应该有数据 await page.locator('.nb-sub-table-addNew').click(); await page.getByTestId('select-object-multiple').click(); - await expect(page.getByRole('option', { name: record1.singleLineText, exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: record1.singleLineText, exact: true }).last()).toBeVisible(); // 2. 为该关系字段设置一个数据范围后,下拉框中应该有一个匹配项 await page.getByRole('button', { name: 'manyToMany1', exact: true }).hover(); @@ -1009,7 +1009,7 @@ test.describe('table column schema settings', () => { await page.reload(); await page.locator('.nb-sub-table-addNew').click(); await page.getByTestId('select-object-multiple').click(); - await expect(page.getByRole('option', { name: record1.singleLineText, exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: record1.singleLineText, exact: true }).last()).toBeVisible(); }); test('fixed column', async ({ page, mockPage }) => { diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts index ffaf80aa15..3ca864e3c2 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts @@ -3824,10 +3824,11 @@ export const T4334 = { }, }, 'x-uid': 'ribk031tkp8', - 'x-async': false, + 'x-async': true, 'x-index': 1, }, }, + 'x-uid': '1j5z1j5z1j5', }, }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx index 32c931e8f3..ed391e9210 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx @@ -141,6 +141,7 @@ export const useTableBlockProps = () => { const storedFilter = block.service.params?.[1]?.filters || {}; if (selectedRow.includes(record[tableBlockContextBasicValue.rowKey])) { + block.clearSelection?.(); if (block.dataLoadingMode === 'manual') { return block.clearData(); } diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx index da441e8f62..ac79a00269 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx @@ -25,6 +25,33 @@ import { setDefaultSortingRulesSchemaSettingsItem } from '../../../../schema-set import { setTheDataScopeSchemaSettingsItem } from '../../../../schema-settings/setTheDataScopeSchemaSettingsItem'; import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider'; import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem'; +import { SchemaSettingsItemType } from '../../../../application'; + +const enabledIndexColumn: SchemaSettingsItemType = { + name: 'enableIndexColumn', + type: 'switch', + useComponentProps: () => { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + return { + title: t('Enable index column'), + checked: field.decoratorProps.enableSelectColumn !== false, + onChange: async (enableIndexÏColumn) => { + field.decoratorProps = field.decoratorProps || {}; + field.decoratorProps.enableIndexÏColumn = enableIndexÏColumn; + fieldSchema['x-decorator-props'].enableIndexÏColumn = enableIndexÏColumn; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, +}; export const tableBlockSettings = new SchemaSettings({ name: 'blockSettings:table', @@ -147,6 +174,7 @@ export const tableBlockSettings = new SchemaSettings({ return field.decoratorProps.dragSort; }, }, + enabledIndexColumn, setTheDataScopeSchemaSettingsItem, setDefaultSortingRulesSchemaSettingsItem, setDataLoadingModeSettingsItem, diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/tableColumnSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/tableColumnSettings.tsx index ed9952bbfd..4868248880 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/tableColumnSettings.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/tableColumnSettings.tsx @@ -82,6 +82,43 @@ export const tableColumnSettings = new SchemaSettings({ }; }, }, + { + name: 'editTooltip', + type: 'modal', + useComponentProps() { + const { t } = useTranslation(); + const { dn } = useDesignable(); + const field = useField(); + const columnSchema = useFieldSchema(); + + return { + title: t('Edit tooltip'), + schema: { + type: 'object', + title: t('Edit tooltip'), + properties: { + tooltip: { + default: columnSchema?.['x-component-props']?.tooltip || '', + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit({ tooltip }) { + field.componentProps.tooltip = tooltip; + columnSchema['x-component-props'] = columnSchema['x-component-props'] || {}; + columnSchema['x-component-props']['tooltip'] = tooltip; + dn.emit('patch', { + schema: { + 'x-uid': columnSchema['x-uid'], + 'x-component-props': columnSchema['x-component-props'], + }, + }); + }, + }; + }, + }, { name: 'style', Component: (props) => { @@ -161,11 +198,12 @@ export const tableColumnSettings = new SchemaSettings({ const { getInterface } = useCollectionManager_deprecated(); const interfaceCfg = getInterface(collectionField?.interface); const { currentMode } = useAssociationFieldContext(); - return ( interfaceCfg?.sortable === true && !currentMode && - (collection?.name === collectionField?.collectionName || !collectionField?.collectionName) + (collection?.name === collectionField?.collectionName || + !collectionField?.collectionName || + collectionField?.inherit) ); }, useComponentProps() { diff --git a/packages/core/client/src/modules/blocks/filter-blocks/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/__e2e__/schemaInitializer.test.ts index ebdc65a7c9..3d76b13799 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/filter-blocks/__e2e__/schemaInitializer.test.ts @@ -35,7 +35,11 @@ test.describe('where filter block can be added', () => { // 3. 与 Table、Details、List、GridCard 等区块建立连接 const connectByForm = async (name: string) => { + await page + .getByLabel('designer-schema-settings-CardItem-blockSettings:filterForm-users') + .waitFor({ state: 'hidden' }); await page.getByLabel('block-item-CardItem-users-filter-form').hover(); + await page.getByRole('menuitem', { name: 'Connect data blocks right' }).waitFor({ state: 'detached' }); await page.getByLabel('designer-schema-settings-CardItem-blockSettings:filterForm-users').hover(); await page.getByRole('menuitem', { name: 'Connect data blocks right' }).hover(); await page.getByRole('menuitem', { name }).click(); @@ -43,6 +47,7 @@ test.describe('where filter block can be added', () => { const connectByCollapse = async (name: string) => { await page.mouse.move(-500, 0); await page.getByLabel('block-item-CardItem-users-filter-collapse').hover(); + await page.getByRole('menuitem', { name: 'Connect data blocks right' }).waitFor({ state: 'detached' }); await page.getByLabel('designer-schema-settings-CardItem-blockSettings:filterCollapse-users').hover(); await page.getByRole('menuitem', { name: 'Connect data blocks right' }).hover(); await page.getByRole('menuitem', { name }).click(); @@ -150,7 +155,9 @@ test.describe('where filter block can be added', () => { } // 2. 测试用表单筛选其它区块 + await page.getByRole('menuitem', { name: 'Form right' }).waitFor({ state: 'detached' }); await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'Users' }).waitFor({ state: 'detached' }); await page.getByRole('menuitem', { name: 'Form right' }).hover(); await page.getByRole('menuitem', { name: 'Users' }).click(); await page.getByLabel('schema-initializer-Grid-filterForm:configureFields-users').hover(); diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/schemaSettings.test.ts index 5bfbfa7818..04b3bf5f91 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/schemaSettings.test.ts @@ -46,7 +46,7 @@ test.describe('collapse schema settings', () => { await page.getByRole('menuitem', { name: 'General' }).click(); // 点击一个选项,进行筛选 - await page.getByRole('button', { name: 'right singleSelect search' }).click(); + await page.getByRole('button', { name: 'collapsed singleSelect search' }).click(); await page.getByLabel('block-item-CardItem-general-filter-collapse').getByText('Option1').click(); // 注意:在本地运行时,由于运行结束后不会清空之前创建的数据,所以在第一次运行之后,下面会报错。如果遇到这种情况,可以先不管 diff --git a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/autoFilterWhenSettingDefaultValue.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/autoFilterWhenSettingDefaultValue.test.ts index 605c7e64a9..0a420f3da1 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/autoFilterWhenSettingDefaultValue.test.ts +++ b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/autoFilterWhenSettingDefaultValue.test.ts @@ -126,7 +126,7 @@ test.describe('filter form', () => { y: 10, }, }); - await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible(); // 4. 此时点击 Reset 按钮,应该只显示一条数据,因为会把 nickname 的值重置为 {{$user.nickname}} await page.getByLabel('action-Action-Reset-users-').click({ @@ -152,6 +152,6 @@ test.describe('filter form', () => { y: 10, }, }); - await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible(); }); }); diff --git a/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/dataScope.test.ts b/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/dataScope.test.ts index eda935801a..cf9444da71 100644 --- a/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/dataScope.test.ts +++ b/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/dataScope.test.ts @@ -53,7 +53,7 @@ test.describe('AssociationSelect ', () => { .getByLabel('block-item-CollectionField-test-form-test.b-b') .getByTestId('select-object-multiple') .click(); - await expect(page.getByText('No data')).toBeVisible(); + await expect(page.getByText('No data').last()).toBeVisible(); // 2. 当给字段 a 选择一个值后,字段 b 的下拉列表中会显示符合条件的值 await page diff --git a/packages/core/client/src/modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings.tsx index 8730b493fa..6c6f2af2d8 100644 --- a/packages/core/client/src/modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings.tsx @@ -17,6 +17,7 @@ import { useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../.. import { isSubMode } from '../../../../schema-component/antd/association-field/util'; import { useTitleFieldOptions } from '../../../../schema-component/antd/form-item/FormItem.Settings'; import { ellipsisSettingsItem } from '../Input/inputComponentSettings'; +import { setTheDataScope } from '../Select/selectComponentFieldSettings'; const fieldComponent: any = { name: 'fieldComponent', @@ -100,5 +101,5 @@ const titleField: any = { export const cascadeSelectComponentFieldSettings = new SchemaSettings({ name: 'fieldSettings:component:CascadeSelect', - items: [fieldComponent, titleField, ellipsisSettingsItem], + items: [fieldComponent, titleField, ellipsisSettingsItem, setTheDataScope], }); diff --git a/packages/core/client/src/modules/fields/component/Select/__e2e__/selectDataScope.test.ts b/packages/core/client/src/modules/fields/component/Select/__e2e__/selectDataScope.test.ts index 368503878c..341e3be388 100644 --- a/packages/core/client/src/modules/fields/component/Select/__e2e__/selectDataScope.test.ts +++ b/packages/core/client/src/modules/fields/component/Select/__e2e__/selectDataScope.test.ts @@ -34,6 +34,6 @@ test.describe('data scope of component Select', () => { await page.getByTestId('select-object-multiple').click(); await expect(page.getByRole('option', { name: 'admin' })).toBeHidden(); await expect(page.getByRole('option', { name: 'member' })).toBeHidden(); - await expect(page.getByText('No data')).toBeVisible(); + await expect(page.getByText('No data').last()).toBeVisible(); }); }); diff --git a/packages/core/client/src/modules/fields/component/Select/__e2e__/selectOptionsInLinkageRule.test.ts b/packages/core/client/src/modules/fields/component/Select/__e2e__/selectOptionsInLinkageRule.test.ts new file mode 100644 index 0000000000..0684ebf21a --- /dev/null +++ b/packages/core/client/src/modules/fields/component/Select/__e2e__/selectOptionsInLinkageRule.test.ts @@ -0,0 +1,33 @@ +/** + * 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 { expect, test } from '@nocobase/test/e2e'; +import { oneFormWithSelectField } from './templatesOfBug'; + +test.describe('options of Select field in linkage rule', () => { + test('options change with linkage rule ', async ({ page, mockPage }) => { + await mockPage(oneFormWithSelectField).goto(); + // 联动规则控制选项 + await page.getByLabel('block-item-CardItem-general-').hover(); + await page.getByLabel('block-item-CollectionField-').click(); + await expect(page.getByRole('option', { name: 'option2' })).toBeVisible(); + await expect(page.getByRole('option', { name: 'option3' })).not.toBeVisible(); + await page.getByRole('option', { name: 'option2' }).click(); + + // 去掉联动规则恢复选项 + await page.getByLabel('block-item-CardItem-general-').hover(); + await page.getByLabel('designer-schema-settings-CardItem-blockSettings:createForm-general').hover(); + await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); + await page.getByRole('switch', { name: 'On Off' }).click(); + await page.getByRole('button', { name: 'OK' }).click(); + await page.reload(); + await expect(page.getByRole('option', { name: 'option2' })).toBeVisible(); + await expect(page.getByRole('option', { name: 'option3' })).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/modules/fields/component/Select/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/fields/component/Select/__e2e__/templatesOfBug.ts index 1d90fe8ebf..dc2e7f36cf 100644 --- a/packages/core/client/src/modules/fields/component/Select/__e2e__/templatesOfBug.ts +++ b/packages/core/client/src/modules/fields/component/Select/__e2e__/templatesOfBug.ts @@ -7,6 +7,8 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { generalWithM2oSingleSelect } from '@nocobase/test/e2e'; + export const T3867 = { pageSchema: { _isJSONSchemaObject: true, @@ -581,3 +583,176 @@ export const oneFormWithSubTableSelectField = { }, ], }; + +export const oneFormWithSelectField = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + iza2br2wzq4: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + dc4lkvej93w: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.7.0-beta.1', + properties: { + jrfai5z5a4e: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.7.0-beta.1', + properties: { + bph3a0yrmvp: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': 'general:create', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'general', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + 'x-app-version': '1.7.0-beta.1', + properties: { + '2ef5ea23d4b': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + 'x-app-version': '1.7.0-beta.1', + properties: { + grid: { + 'x-uid': 'oqk574y4mx2', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.7.0-beta.1', + 'x-linkage-rules': [ + { + condition: { + $and: [], + }, + actions: [ + { + targetFields: ['singleSelect'], + operator: 'options', + value: { + value: ['option2'], + }, + }, + ], + }, + ], + properties: { + '1ijfrrxqs4c': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.7.0-beta.1', + properties: { + '8j09c65cp2x': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.7.0-beta.1', + properties: { + singleSelect: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'general.singleSelect', + 'x-component-props': { + style: { + width: '100%', + }, + }, + 'x-app-version': '1.7.0-beta.1', + 'x-uid': 'dxsbc80ewso', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'la6qedc9qwx', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ijsbxa4yys7', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + kpp1azsxbzy: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'createForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.7.0-beta.1', + 'x-uid': '1n56lepnve8', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'lf74cpc3tub', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'mhxlfdk74jy', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'obyvsr0s0nx', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 's6jd8p9h28q', + 'x-async': false, + 'x-index': 1, + }, + }, + name: '8btun7jzkrk', + 'x-uid': 'eyjf89l83a1', + 'x-async': true, + 'x-index': 1, + }, + }, + 'x-uid': 'hizrr7jzogr', + 'x-async': true, + 'x-index': 1, + }, + collections: generalWithM2oSingleSelect, +}; diff --git a/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx index 2823e9d62f..dc3ff40a10 100644 --- a/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx @@ -228,7 +228,7 @@ const setDefaultSortingRules = { Component: SchemaSettingsSortingRule, }; -const setTheDataScope: any = { +export const setTheDataScope: any = { name: 'setTheDataScope', Component: SchemaSettingsDataScope, useComponentProps() { diff --git a/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx index d5b5ae25d3..55e1d28991 100644 --- a/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx @@ -28,7 +28,33 @@ import { isSubMode } from '../../../../schema-component/antd/association-field/u import { useIsAssociationField } from '../../../../schema-component/antd/form-item'; import { FormLinkageRules } from '../../../../schema-settings/LinkageRules'; import { SchemaSettingsLinkageRules } from '../../../../schema-settings/SchemaSettings'; +import { SchemaSettingsItemType } from '../../../../application'; +const enabledIndexColumn: SchemaSettingsItemType = { + name: 'enableIndexColumn', + type: 'switch', + useComponentProps: () => { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + return { + title: t('Enable index column'), + checked: field.componentProps.enableIndexÏColumn !== false, + onChange: async (enableIndexÏColumn) => { + field.componentProps = field.componentProps || {}; + field.componentProps.enableIndexÏColumn = enableIndexÏColumn; + fieldSchema['x-component-props'].enableIndexÏColumn = enableIndexÏColumn; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': fieldSchema['x-component-props'], + }, + }); + }, + }; + }, +}; const fieldComponent: any = { name: 'fieldComponent', type: 'select', @@ -365,6 +391,7 @@ export const subTablePopoverComponentFieldSettings = new SchemaSettings({ allowSelectExistingRecord, allowDisassociation, setDefaultSortingRules, + enabledIndexColumn, linkageRules, recordPerPage, ], diff --git a/packages/core/client/src/modules/menu/GroupItem.tsx b/packages/core/client/src/modules/menu/GroupItem.tsx index 2571af23f8..e2acf4b353 100644 --- a/packages/core/client/src/modules/menu/GroupItem.tsx +++ b/packages/core/client/src/modules/menu/GroupItem.tsx @@ -12,7 +12,7 @@ import { SchemaOptionsContext } from '@formily/react'; import { uid } from '@formily/shared'; import React, { useCallback, useContext } from 'react'; import { useTranslation } from 'react-i18next'; -import { SchemaInitializerItem, useSchemaInitializer } from '../../application'; +import { SchemaInitializerItem } from '../../application'; import { useGlobalTheme } from '../../global-theme'; import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema'; import { @@ -25,7 +25,6 @@ import { import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers'; export const GroupItem = () => { - const { insert } = useSchemaInitializer(); const { t } = useTranslation(); const options = useContext(SchemaOptionsContext); const { theme } = useGlobalTheme(); @@ -69,30 +68,13 @@ export const GroupItem = () => { const schemaUid = uid(); // 创建一个路由到 desktopRoutes 表中 - const { data } = await createRoute({ + await createRoute({ type: NocoBaseDesktopRouteType.group, title, icon, parentId: parentRoute?.id, schemaUid, }); - - // 同时插入一个对应的 Schema - insert(getGroupMenuSchema({ title, icon, schemaUid, route: data?.data })); - }, [insert, options.components, options.scope, t, theme]); + }, [options.components, options.scope, t, theme]); return ; }; - -export function getGroupMenuSchema({ title, icon, schemaUid, route = undefined }) { - return { - type: 'void', - title, - 'x-component': 'Menu.SubMenu', - 'x-decorator': 'ACLMenuItemProvider', - 'x-component-props': { - icon, - }, - 'x-uid': schemaUid, - __route__: route, - }; -} diff --git a/packages/core/client/src/modules/menu/LinkMenuItem.tsx b/packages/core/client/src/modules/menu/LinkMenuItem.tsx index 93b2c6ade8..8217f7de14 100644 --- a/packages/core/client/src/modules/menu/LinkMenuItem.tsx +++ b/packages/core/client/src/modules/menu/LinkMenuItem.tsx @@ -9,12 +9,11 @@ import { FormLayout } from '@formily/antd-v5'; import { SchemaOptionsContext } from '@formily/react'; -import { uid } from '@formily/shared'; import { createMemoryHistory } from 'history'; import React, { useCallback, useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { Router } from 'react-router-dom'; -import { SchemaInitializerItem, useSchemaInitializer } from '../../application'; +import { SchemaInitializerItem } from '../../application'; import { useGlobalTheme } from '../../global-theme'; import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema'; import { @@ -28,7 +27,6 @@ import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers import { useURLAndHTMLSchema } from '../actions/link/useURLAndHTMLSchema'; export const LinkMenuItem = () => { - const { insert } = useSchemaInitializer(); const { t } = useTranslation(); const options = useContext(SchemaOptionsContext); const { theme } = useGlobalTheme(); @@ -75,40 +73,19 @@ export const LinkMenuItem = () => { initialValues: {}, }); const { title, href, params, icon } = values; - const schemaUid = uid(); // 创建一个路由到 desktopRoutes 表中 - const { data } = await createRoute({ + await createRoute({ type: NocoBaseDesktopRouteType.link, - title: values.title, - icon: values.icon, + title, + icon, parentId: parentRoute?.id, - schemaUid, options: { href, params, }, }); - - // 同时插入一个对应的 Schema - insert(getLinkMenuSchema({ title, icon, schemaUid, href, params, route: data?.data })); - }, [insert, options.components, options.scope, t, theme]); + }, [options.components, options.scope, t, theme]); return ; }; - -export function getLinkMenuSchema({ title, icon, schemaUid, href, params, route = undefined }) { - return { - type: 'void', - title, - 'x-component': 'Menu.URL', - 'x-decorator': 'ACLMenuItemProvider', - 'x-component-props': { - icon, - href, - params, - }, - 'x-uid': schemaUid, - __route__: route, - }; -} diff --git a/packages/core/client/src/modules/menu/PageMenuItem.tsx b/packages/core/client/src/modules/menu/PageMenuItem.tsx index 675de5746a..819e11592c 100644 --- a/packages/core/client/src/modules/menu/PageMenuItem.tsx +++ b/packages/core/client/src/modules/menu/PageMenuItem.tsx @@ -12,7 +12,8 @@ import { SchemaOptionsContext } from '@formily/react'; import { uid } from '@formily/shared'; import React, { useCallback, useContext } from 'react'; import { useTranslation } from 'react-i18next'; -import { SchemaInitializerItem, useSchemaInitializer } from '../../application'; +import { useAPIClient } from '../../api-client/hooks/useAPIClient'; +import { SchemaInitializerItem } from '../../application'; import { useGlobalTheme } from '../../global-theme'; import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema'; import { @@ -24,14 +25,28 @@ import { } from '../../schema-component'; import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers'; +export const useInsertPageSchema = () => { + const api = useAPIClient(); + return useCallback( + async (schema) => { + await api.request({ + method: 'POST', + url: '/uiSchemas:insert', + data: schema, + }); + }, + [api], + ); +}; + export const PageMenuItem = () => { - const { insert } = useSchemaInitializer(); const { t } = useTranslation(); const options = useContext(SchemaOptionsContext); const { theme } = useGlobalTheme(); const { componentCls, hashId } = useStyles(); const parentRoute = useParentRoute(); const { createRoute } = useNocoBaseRoutes(); + const insertPageSchema = useInsertPageSchema(); const handleClick = useCallback(async () => { const values = await FormDialog( @@ -65,16 +80,13 @@ export const PageMenuItem = () => { ).open({ initialValues: {}, }); - const { title, icon } = values; const menuSchemaUid = uid(); const pageSchemaUid = uid(); const tabSchemaUid = uid(); const tabSchemaName = uid(); // 创建一个路由到 desktopRoutes 表中 - const { - data: { data: route }, - } = await createRoute({ + await createRoute({ type: NocoBaseDesktopRouteType.page, title: values.title, icon: values.icon, @@ -93,46 +105,25 @@ export const PageMenuItem = () => { }); // 同时插入一个对应的 Schema - insert(getPageMenuSchema({ title, icon, pageSchemaUid, tabSchemaUid, menuSchemaUid, tabSchemaName, route })); - }, [createRoute, insert, options?.components, options?.scope, parentRoute?.id, t, theme]); + insertPageSchema(getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName })); + }, [createRoute, insertPageSchema, options?.components, options?.scope, parentRoute?.id, t, theme]); return ; }; -export function getPageMenuSchema({ - title, - icon, - pageSchemaUid, - tabSchemaUid, - menuSchemaUid, - tabSchemaName, - route = undefined, -}) { +export function getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }) { return { type: 'void', - title, - 'x-component': 'Menu.Item', - 'x-decorator': 'ACLMenuItemProvider', - 'x-component-props': { - icon, - }, + 'x-component': 'Page', properties: { - page: { + [tabSchemaName]: { type: 'void', - 'x-component': 'Page', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: {}, + 'x-uid': tabSchemaUid, 'x-async': true, - properties: { - [tabSchemaName]: { - type: 'void', - 'x-component': 'Grid', - 'x-initializer': 'page:addBlock', - properties: {}, - 'x-uid': tabSchemaUid, - }, - }, - 'x-uid': pageSchemaUid, }, }, - 'x-uid': menuSchemaUid, - __route__: route, + 'x-uid': pageSchemaUid, }; } diff --git a/packages/core/client/src/modules/menu/__e2e__/dragAndDrop.test.ts b/packages/core/client/src/modules/menu/__e2e__/dragAndDrop.test.ts index 770cefd04d..5ca3b7f0cc 100644 --- a/packages/core/client/src/modules/menu/__e2e__/dragAndDrop.test.ts +++ b/packages/core/client/src/modules/menu/__e2e__/dragAndDrop.test.ts @@ -18,7 +18,7 @@ test('single page', async ({ page, mockPage }) => { await mockPage({ name: pageTitle2 }).goto(); await page.getByRole('menu').getByText(pageTitle1).click(); await page.getByRole('menu').getByText(pageTitle1).hover(); - await page.getByLabel(pageTitle1).getByLabel('designer-schema-settings').hover(); + await page.getByRole('button', { name: 'designer-schema-settings-' }).hover(); await page.getByRole('menuitem', { name: 'Move to' }).click(); await page.getByLabel('block-item-TreeSelect-Target').locator('.ant-select').click(); await page.locator('.ant-select-dropdown').getByText(pageTitle2).click(); diff --git a/packages/core/client/src/modules/menu/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/menu/__e2e__/schemaSettings.test.ts index 786996bb1f..ba6f06a3f7 100644 --- a/packages/core/client/src/modules/menu/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/menu/__e2e__/schemaSettings.test.ts @@ -17,7 +17,7 @@ test.describe('group page side menus schema settings', () => { await expectSettingsMenu({ page, showMenu: () => showSettingsInSide(page, 'group page in side'), - supportedOptions: ['Edit', 'Move to', 'Insert before', 'Insert after', 'Insert inner', 'Delete'], + supportedOptions: ['Edit', 'Edit tooltip', 'Move to', 'Insert before', 'Insert after', 'Insert inner', 'Delete'], }); }); @@ -28,7 +28,7 @@ test.describe('group page side menus schema settings', () => { await expectSettingsMenu({ page, showMenu: () => showSettingsInSide(page, 'link page in side'), - supportedOptions: ['Edit', 'Move to', 'Insert before', 'Insert after', 'Delete'], + supportedOptions: ['Edit', 'Edit tooltip', 'Move to', 'Insert before', 'Insert after', 'Delete'], }); }); @@ -39,7 +39,7 @@ test.describe('group page side menus schema settings', () => { await expectSettingsMenu({ page, showMenu: () => showSettingsInSide(page, 'single page in side'), - supportedOptions: ['Edit', 'Move to', 'Insert before', 'Insert after', 'Delete'], + supportedOptions: ['Edit', 'Edit tooltip', 'Move to', 'Insert before', 'Insert after', 'Delete'], }); }); }); diff --git a/packages/core/client/src/modules/menu/__e2e__/schemaSettings1.test.ts b/packages/core/client/src/modules/menu/__e2e__/schemaSettings1.test.ts index b52fcb9cb4..ef5799f111 100644 --- a/packages/core/client/src/modules/menu/__e2e__/schemaSettings1.test.ts +++ b/packages/core/client/src/modules/menu/__e2e__/schemaSettings1.test.ts @@ -13,7 +13,7 @@ test.describe('group page menus schema settings', () => { test('edit', async ({ page, mockPage }) => { await mockPage({ type: 'group', name: 'group page' }).goto(); await showSettings(page, 'group page'); - await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page.getByRole('menuitem', { name: 'Edit', exact: true }).click(); await page.mouse.move(300, 0); // 设置一个新名称 diff --git a/packages/core/client/src/modules/page/__e2e__/router.test.ts b/packages/core/client/src/modules/page/__e2e__/router.test.ts index d9aa85995c..4692e7a112 100644 --- a/packages/core/client/src/modules/page/__e2e__/router.test.ts +++ b/packages/core/client/src/modules/page/__e2e__/router.test.ts @@ -20,14 +20,14 @@ test.describe('router', () => { await expect(page.getByText('This is tab2.')).toBeVisible(); // 2. 点击 tab1 应该跳转到 tab1,并使用新版 URL - await page.getByText('tab1').click(); + await page.getByText('tab1', { exact: true }).click(); await expect(page.getByText('This is tab1.')).toBeVisible(); - expect(page.url()).toMatch(new RegExp(`${pageUrl}/tabs/u4earq3d9go`)); + expect(page.url()).toMatch(new RegExp(`${pageUrl}/tabs/xbx6zg90ij2`)); // 3. 点击 tab2 应该跳转到 tab2,并使用新版 URL - await page.getByText('tab2').click(); + await page.getByText('tab2', { exact: true }).click(); await expect(page.getByText('This is tab2.')).toBeVisible(); - expect(page.url()).toMatch(new RegExp(`${pageUrl}/tabs/bbch3c9b5jl`)); + expect(page.url()).toMatch(new RegExp(`${pageUrl}/tabs/qhjdmy9nk6q`)); // 4. 使用不带 tab 参数的 URL,应该默认显示第一个 tab await nocoPage.goto(); diff --git a/packages/core/client/src/modules/page/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/page/__e2e__/schemaSettings.test.ts index ed8c0a7879..8c43d6fc56 100644 --- a/packages/core/client/src/modules/page/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/page/__e2e__/schemaSettings.test.ts @@ -63,9 +63,11 @@ test.describe('page schema settings', () => { await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').fill('new tab'); // 选择一个图标 await page.getByRole('button', { name: 'Select icon' }).click(); + await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1); await page.getByLabel('account-book').locator('svg').click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); await expect(page.getByText('new tab')).toBeVisible(); + await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1); await expect(page.getByLabel('account-book').locator('svg')).toBeVisible(); }); }); @@ -92,10 +94,12 @@ test.describe('tabs schema settings', () => { await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').click(); await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').fill('new name of page tab'); await page.getByRole('button', { name: 'Select icon' }).click(); + await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1); await page.getByLabel('account-book').locator('svg').click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); await expect(page.getByText('new name of page tab')).toBeVisible(); + await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1); await expect(page.getByLabel('account-book').locator('svg')).toBeVisible(); }); diff --git a/packages/core/client/src/modules/popup/PopupContextProvider.tsx b/packages/core/client/src/modules/popup/PopupContextProvider.tsx index 2b034a4477..400e1d005d 100644 --- a/packages/core/client/src/modules/popup/PopupContextProvider.tsx +++ b/packages/core/client/src/modules/popup/PopupContextProvider.tsx @@ -20,6 +20,8 @@ import { PopupVisibleProvider, PopupVisibleProviderContext } from '../../schema- export const PopupContextProvider: React.FC<{ visible?: boolean; setVisible?: (visible: boolean) => void; + openMode?: string; + openSize?: string; }> = (props) => { const { visible: visibleFromProps, setVisible: setVisibleFromProps } = props; const [visible, setVisible] = useState(false); @@ -37,8 +39,8 @@ export const PopupContextProvider: React.FC<{ }, [setVisibleFromProps, setVisibleWithURL], ); - const openMode = fieldSchema['x-component-props']?.['openMode'] || 'drawer'; - const openSize = fieldSchema['x-component-props']?.['openSize']; + const openMode = props.openMode || fieldSchema['x-component-props']?.['openMode'] || 'drawer'; + const openSize = props.openSize || fieldSchema['x-component-props']?.['openSize']; return ( diff --git a/packages/core/client/src/modules/popup/__e2e__/deletedPopups.test.ts b/packages/core/client/src/modules/popup/__e2e__/deletedPopups.test.ts index af82147663..60f9964cac 100644 --- a/packages/core/client/src/modules/popup/__e2e__/deletedPopups.test.ts +++ b/packages/core/client/src/modules/popup/__e2e__/deletedPopups.test.ts @@ -13,12 +13,11 @@ test.describe('deleted popups', () => { test('should display error info when deleted popups', async ({ page, mockPage }) => { const nocoPage = await mockPage().waitForInit(); const url = await nocoPage.getUrl(); - - await page.goto( + const path = url + - '/popups/vygn5ile3xz/filterbytk/1/popups/n24hos465bj/filterbytk/admin/sourceid/1/popups/s32h1ed5g9i/filterbytk/admin/sourceid/1', - ); + '/popups/vygn5ile3xz/filterbytk/1/popups/n24hos465bj/filterbytk/admin/sourceid/1/popups/s32h1ed5g9i/filterbytk/admin/sourceid/1'; + await page.goto(path); await expect(page.getByText('Sorry, the page you visited does not exist.')).toHaveCount(3); // close the popups diff --git a/packages/core/client/src/modules/popup/__e2e__/router.test.ts b/packages/core/client/src/modules/popup/__e2e__/router.test.ts index 690b235b89..1d534bc912 100644 --- a/packages/core/client/src/modules/popup/__e2e__/router.test.ts +++ b/packages/core/client/src/modules/popup/__e2e__/router.test.ts @@ -18,7 +18,7 @@ test.describe('popup router', () => { }).waitForInit(); const url = await nocoPage.getUrl(); - // 直接跳转到子页面,然后点击返回按钮,查看是否能返回到上一级页面 + // Directly navigate to the subpage, then click the back button to check if it can return to the parent page await page.goto( url + '/popups/56tsj7l3k35/filterbytk/1/popups/bd3nizznkdw/filterbytk/member/sourceid/1/popups/1ct9qd9jlbm/filterbytk/member/sourceid/1', diff --git a/packages/core/client/src/modules/popup/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/popup/__e2e__/schemaInitializer.test.ts index 6ec7d36c03..d684443399 100644 --- a/packages/core/client/src/modules/popup/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/popup/__e2e__/schemaInitializer.test.ts @@ -55,7 +55,7 @@ test.describe('add blocks to the popup', () => { // 通过 Association records 创建一个关系区块 await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByRole('menuitem', { name: 'Details right' }).hover(); - await page.getByRole('menuitem', { name: 'Associated records' }).hover(); + await page.getByRole('menuitem', { name: 'Associated records' }).last().hover(); await page.getByRole('menuitem', { name: 'Roles' }).click(); await page.getByLabel('schema-initializer-Grid-details:configureFields-roles').hover(); await page.getByRole('menuitem', { name: 'Role UID' }).click(); @@ -87,7 +87,7 @@ test.describe('add blocks to the popup', () => { // 通过 Association records 创建关系区块 await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover(); - await page.getByRole('menuitem', { name: 'Associated records' }).hover(); + await page.getByRole('menuitem', { name: 'Associated records' }).last().hover(); await page.getByRole('menuitem', { name: 'manyToMany' }).click(); await page.mouse.move(-300, 0); await page @@ -135,7 +135,7 @@ test.describe('add blocks to the popup', () => { // 通过 Association records 创建一个关系区块 await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover(); - await page.getByRole('menuitem', { name: 'Associated records' }).hover(); + await page.getByRole('menuitem', { name: 'Associated records' }).last().hover(); await page.getByRole('menuitem', { name: 'Roles' }).click(); await page .getByTestId('drawer-Action.Container-users-View record') diff --git a/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts b/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts index 7d9b481842..5efa1553bc 100644 --- a/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts +++ b/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts @@ -124,10 +124,13 @@ test.describe('where to open a popup and what can be added to it', () => { async function addBlock(names: string[]) { await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.waitForTimeout(500); for (let i = 0; i < names.length - 1; i++) { const name = names[i]; await page.getByRole('menuitem', { name }).hover(); + await page.waitForTimeout(500); } + await expect(page.getByRole('menuitem', { name: names[names.length - 1] })).toHaveCount(1); await page.getByRole('menuitem', { name: names[names.length - 1] }).click(); await page.mouse.move(300, 0); } @@ -206,13 +209,14 @@ test.describe('where to open a popup and what can be added to it', () => { await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); await page.getByRole('menuitem', { name: 'Details' }).hover(); - await page.getByRole('menuitem', { name: 'Associated records' }).hover(); + await page.getByRole('menuitem', { name: 'Associated records' }).last().hover(); await page.getByRole('menuitem', { name: 'Many to one' }).click(); await page.mouse.move(300, 0); await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible(); await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover(); + await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1); await page.getByRole('menuitem', { name: 'Associated records' }).hover(); await page.getByRole('menuitem', { name: 'One to many' }).click(); await page.mouse.move(300, 0); @@ -272,13 +276,14 @@ test.describe('where to open a popup and what can be added to it', () => { await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); await page.getByRole('menuitem', { name: 'Details' }).hover(); - await page.getByRole('menuitem', { name: 'Associated records' }).hover(); + await page.getByRole('menuitem', { name: 'Associated records' }).last().hover(); await page.getByRole('menuitem', { name: 'Many to one' }).click(); await page.mouse.move(300, 0); await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible(); await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover(); + await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1); await page.getByRole('menuitem', { name: 'Associated records' }).hover(); await page.getByRole('menuitem', { name: 'One to many' }).click(); await page.mouse.move(300, 0); diff --git a/packages/core/client/src/modules/popup/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/popup/__e2e__/schemaSettings.test.ts index 7da9ed5c43..23c5bbfa7c 100644 --- a/packages/core/client/src/modules/popup/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/popup/__e2e__/schemaSettings.test.ts @@ -44,6 +44,7 @@ test.describe('tabs schema settings', () => { await page.getByRole('button', { name: 'OK', exact: true }).click(); await expect(page.getByText('Add new with new name')).toBeVisible(); + await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1); await expect(page.getByLabel('account-book').locator('svg')).toBeVisible(); }); diff --git a/packages/core/client/src/modules/variable/__e2e__/parentObject.test.ts b/packages/core/client/src/modules/variable/__e2e__/parentObject.test.ts index 9e040459b4..d8c825b7ca 100644 --- a/packages/core/client/src/modules/variable/__e2e__/parentObject.test.ts +++ b/packages/core/client/src/modules/variable/__e2e__/parentObject.test.ts @@ -55,6 +55,7 @@ test.describe('variable: parent object', () => { // 1. Use "Current form" and "Parent object" variables in nested subforms and subtables await page.getByLabel('block-item-CollectionField-collection1-form-collection1.m2m1-m2m1').hover(); + await page.getByRole('menuitem', { name: 'Linkage rules' }).waitFor({ state: 'detached' }); await page .getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-collection1-collection1.m2m1', { exact: true, diff --git a/packages/core/client/src/nocobase-buildin-plugin/index.tsx b/packages/core/client/src/nocobase-buildin-plugin/index.tsx index 3caea7ddc7..b9d61e9123 100644 --- a/packages/core/client/src/nocobase-buildin-plugin/index.tsx +++ b/packages/core/client/src/nocobase-buildin-plugin/index.tsx @@ -11,6 +11,7 @@ import { DisconnectOutlined, LoadingOutlined } from '@ant-design/icons'; import { css } from '@emotion/css'; import { observer } from '@formily/reactive-react'; import { getSubAppName } from '@nocobase/sdk'; +import { tval } from '@nocobase/utils/client'; import { Button, Modal, Result, Spin } from 'antd'; import React, { FC } from 'react'; import { Navigate, useNavigate } from 'react-router-dom'; @@ -32,7 +33,6 @@ import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates'; import { SystemSettingsPlugin } from '../system-settings'; import { CurrentUserProvider, CurrentUserSettingsMenuProvider } from '../user'; import { LocalePlugin } from './plugins/LocalePlugin'; -import { tval } from '@nocobase/utils/client'; const AppSpin = () => { return ( @@ -251,7 +251,7 @@ const AppMaintainingDialog: FC<{ app: Application; error: Error }> = observer( { displayName: 'AppMaintainingDialog' }, ); -const AppNotFound = () => { +export const AppNotFound = () => { const navigate = useNavigate(); return ( = (props) => { @@ -25,15 +28,50 @@ export const PinnedPluginListProvider: React.FC<{ items: any }> = (props) => { }; const pinnedPluginListClassName = css` - display: inline-block; + display: inline-flex; + align-items: center; + color: var(--colorTextHeaderMenu); + + .anticon { + color: var(--colorTextHeaderMenu); + } .ant-btn { - border: 0; + display: inline-flex; + align-items: center; + justify-content: center; height: 46px; width: 46px; + padding: 0; + border: 0; border-radius: 0; background: none; color: rgba(255, 255, 255, 0.65); + vertical-align: middle; + + a { + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + } + + .ant-badge { + color: rgba(255, 255, 255, 0.65); + .anticon { + display: inline-block; + vertical-align: middle; + line-height: 1em; + font-size: initial; + } + > sup { + height: 10px; + line-height: 10px; + font-size: 8px; + } + } + &:hover { background: rgba(255, 255, 255, 0.1) !important; } @@ -44,6 +82,12 @@ const pinnedPluginListClassName = css` } `; +const dividerTheme = { + token: { + colorSplit: 'rgba(255, 255, 255, 0.1)', + }, +}; + export const PinnedPluginList = React.memo(() => { const { allowAll, snippets } = useACLRoleContext(); const getSnippetsAllow = (aclKey) => { @@ -61,6 +105,11 @@ export const PinnedPluginList = React.memo(() => { const Action = get(components, ctx.items[key].component); return Action ? : null; })} + + + + + ); }); diff --git a/packages/core/client/src/pm/PluginManager.tsx b/packages/core/client/src/pm/PluginManager.tsx index 76365d1537..6ffe8eb425 100644 --- a/packages/core/client/src/pm/PluginManager.tsx +++ b/packages/core/client/src/pm/PluginManager.tsx @@ -10,7 +10,7 @@ export * from './PluginManagerLink'; import { PageHeader } from '@ant-design/pro-layout'; import { useDebounce } from 'ahooks'; -import { Button, Col, Divider, Input, List, Modal, Result, Row, Space, Spin, Table, Tabs } from 'antd'; +import { Button, Col, Divider, Input, List, Modal, Result, Row, Space, Spin, Table, Tabs, TableProps } from 'antd'; import _ from 'lodash'; import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -127,25 +127,27 @@ function BulkEnableButton({ plugins = [] }) { }} size={'small'} pagination={false} - columns={[ - { - title: t('Plugin'), - dataIndex: 'displayName', - ellipsis: true, - }, - { - title: t('Description'), - dataIndex: 'description', - ellipsis: true, - width: 300, - }, - { - title: t('Package name'), - dataIndex: 'packageName', - width: 300, - ellipsis: true, - }, - ]} + columns={ + [ + { + title: t('Plugin'), + dataIndex: 'displayName', + ellipsis: true, + }, + { + title: t('Description'), + dataIndex: 'description', + ellipsis: true, + width: 300, + }, + { + title: t('Package name'), + dataIndex: 'packageName', + width: 300, + ellipsis: true, + }, + ] as TableProps['columns'] + } dataSource={items} /> diff --git a/packages/core/client/src/pm/PluginManagerLink.tsx b/packages/core/client/src/pm/PluginManagerLink.tsx index 07867ce568..2fce194c38 100644 --- a/packages/core/client/src/pm/PluginManagerLink.tsx +++ b/packages/core/client/src/pm/PluginManagerLink.tsx @@ -13,6 +13,7 @@ import React, { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { useApp, useNavigateNoUpdate } from '../application'; +import { useMobileLayout } from '../route-switch/antd/admin-layout'; import { useCompile } from '../schema-component'; import { useToken } from '../style'; @@ -20,6 +21,12 @@ export const PluginManagerLink = () => { const { t } = useTranslation(); const navigate = useNavigateNoUpdate(); const { token } = useToken(); + const { isMobileLayout } = useMobileLayout(); + + if (isMobileLayout) { + return null; + } + return (