Merge branch 'next' into feat/plugin-bulk-filter

This commit is contained in:
Zeke Zhang 2025-04-16 17:57:26 +08:00
commit 3dc71ab222
299 changed files with 11384 additions and 838 deletions

View File

@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.6.20](https://github.com/nocobase/nocobase/compare/v1.6.19...v1.6.20) - 2025-04-14
### 🎉 New Features
- **[Departments]** Make Department, Attachment URL, and Workflow response message plugins free ([#6663](https://github.com/nocobase/nocobase/pull/6663)) by @chenos
### 🐛 Bug Fixes
- **[client]**
- The filter form should not display the "Unsaved changes" prompt ([#6657](https://github.com/nocobase/nocobase/pull/6657)) by @zhangzhonghe
- "allow multiple" option not working for relation field ([#6661](https://github.com/nocobase/nocobase/pull/6661)) by @katherinehhh
- In the filter form, when the filter button is clicked, if there are fields that have not passed validation, the filtering is still triggered ([#6659](https://github.com/nocobase/nocobase/pull/6659)) by @zhangzhonghe
- Switching to the group menu should not jump to a page that has already been hidden in menu ([#6654](https://github.com/nocobase/nocobase/pull/6654)) by @zhangzhonghe
- **[File storage: S3(Pro)]**
- Organize language by @jiannx
- Individual baseurl and public settings, improve S3 pro storage config UX by @jiannx
- **[Migration manager]** the skip auto backup option becomes invalid if environment variable popup appears during migration by @gchust
## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14 ## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14
### 🐛 Bug Fixes ### 🐛 Bug Fixes

View File

@ -5,6 +5,30 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
## [v1.6.20](https://github.com/nocobase/nocobase/compare/v1.6.19...v1.6.20) - 2025-04-14
### 🎉 新特性
- **[部门]** 商业插件部门、附件 URL、工作流响应消息改为免费提供 ([#6663](https://github.com/nocobase/nocobase/pull/6663)) by @chenos
### 🐛 修复
- **[client]**
- 筛选表单不应该显示“未保存修改”提示 ([#6657](https://github.com/nocobase/nocobase/pull/6657)) by @zhangzhonghe
- 筛选表单中关系字段的“允许多选”设置项不生效 ([#6661](https://github.com/nocobase/nocobase/pull/6661)) by @katherinehhh
- 筛选表单中,当点击筛选按钮时,如果有字段未校验通过,依然会触发筛选的问题 ([#6659](https://github.com/nocobase/nocobase/pull/6659)) by @zhangzhonghe
- 切换到分组菜单时,不应该跳转到已经在菜单中被隐藏的页面 ([#6654](https://github.com/nocobase/nocobase/pull/6654)) by @zhangzhonghe
- **[文件存储S3 (Pro)]**
- 整理语言文案 by @jiannx
- baseurl 和 public 设置不再互相关联,改进 S3 pro 存储的配置交互体验 by @jiannx
- **[迁移管理]** 迁移时若弹出环境变量弹窗,跳过自动备份选项会失效 by @gchust
## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14 ## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14
### 🐛 修复 ### 🐛 修复

View File

@ -6,7 +6,7 @@ WORKDIR /app
RUN cd /app \ RUN cd /app \
&& yarn config set network-timeout 600000 -g \ && yarn config set network-timeout 600000 -g \
&& npx -y create-nocobase-app@${CNA_VERSION} my-nocobase-app -a -e APP_ENV=production \ && npx -y create-nocobase-app@${CNA_VERSION} my-nocobase-app --skip-dev-dependencies -a -e APP_ENV=production \
&& cd /app/my-nocobase-app \ && cd /app/my-nocobase-app \
&& yarn install --production && yarn install --production

View File

@ -1,5 +1,5 @@
{ {
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"npmClientArgs": ["--ignore-engines"], "npmClientArgs": ["--ignore-engines"],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
{ {
"name": "@nocobase/cache", "name": "@nocobase/cache",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/lock-manager": "1.7.0-beta.16", "@nocobase/lock-manager": "1.7.0-beta.18",
"bloom-filters": "^3.0.1", "bloom-filters": "^3.0.1",
"cache-manager": "^5.2.4", "cache-manager": "^5.2.4",
"cache-manager-redis-yet": "^4.1.2" "cache-manager-redis-yet": "^4.1.2"

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/cli", "name": "@nocobase/cli",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./src/index.js", "main": "./src/index.js",
@ -8,7 +8,7 @@
"nocobase": "./bin/index.js" "nocobase": "./bin/index.js"
}, },
"dependencies": { "dependencies": {
"@nocobase/app": "1.7.0-beta.16", "@nocobase/app": "1.7.0-beta.18",
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@umijs/utils": "3.5.20", "@umijs/utils": "3.5.20",
"chalk": "^4.1.1", "chalk": "^4.1.1",
@ -18,14 +18,13 @@
"fast-glob": "^3.3.1", "fast-glob": "^3.3.1",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"p-all": "3.0.0", "p-all": "3.0.0",
"pm2": "^5.2.0", "pm2": "^6.0.5",
"portfinder": "^1.0.28", "portfinder": "^1.0.28",
"serve": "^13.0.2",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",
"tsx": "^4.19.0" "tsx": "^4.19.0"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/devtools": "1.7.0-beta.16" "@nocobase/devtools": "1.7.0-beta.18"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

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

View File

@ -234,6 +234,10 @@ export default defineConfig({
"title": "Filter", "title": "Filter",
"link": "/components/filter" "link": "/components/filter"
}, },
{
"title": "LinkageFilter",
"link": "/components/linkage-filter"
},
] ]
}, },
{ {

View File

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

View File

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

View File

@ -546,9 +546,11 @@ export const useFilterBlockActionProps = () => {
const { doFilter } = useDoFilter(); const { doFilter } = useDoFilter();
const actionField = useField(); const actionField = useField();
actionField.data = actionField.data || {}; actionField.data = actionField.data || {};
const form = useForm();
return { return {
async onClick() { async onClick() {
await form.submit();
actionField.data.loading = true; actionField.data.loading = true;
await doFilter(); await doFilter();
actionField.data.loading = false; actionField.data.loading = false;
@ -1580,7 +1582,7 @@ export const getAppends = ({
const fieldNames = getTargetField(item); const fieldNames = getTargetField(item);
// 只应该收集关系字段,只有大于 1 的时候才是关系字段 // 只应该收集关系字段,只有大于 1 的时候才是关系字段
if (fieldNames.length > 1) { if (fieldNames.length > 1 && !item.op) {
appends.add(fieldNames.join('.')); appends.add(fieldNames.join('.'));
} }
}); });

View File

@ -15,6 +15,8 @@ import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/actio
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
import { SchemaSettingsEnableChildCollections } from '../../../schema-settings/SchemaSettings'; import { SchemaSettingsEnableChildCollections } from '../../../schema-settings/SchemaSettings';
import { useOpenModeContext } from '../../popup/OpenModeProvider'; import { useOpenModeContext } from '../../popup/OpenModeProvider';
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
import { useDataBlockProps } from '../../../data-source';
export const addNewActionSettings = new SchemaSettings({ export const addNewActionSettings = new SchemaSettings({
name: 'actionSettings:addNew', name: 'actionSettings:addNew',
@ -27,6 +29,16 @@ export const addNewActionSettings = new SchemaSettings({
return buttonEditorProps; return buttonEditorProps;
}, },
}, },
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{ {
name: 'openMode', name: 'openMode',
Component: SchemaSettingOpenModeSchemaItems, Component: SchemaSettingOpenModeSchemaItems,

View File

@ -15,6 +15,7 @@ import {
SecondConFirm, SecondConFirm,
RefreshDataBlockRequest, RefreshDataBlockRequest,
} from '../../../schema-component/antd/action/Action.Designer'; } from '../../../schema-component/antd/action/Action.Designer';
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
export const bulkDeleteActionSettings = new SchemaSettings({ export const bulkDeleteActionSettings = new SchemaSettings({
name: 'actionSettings:bulkDelete', name: 'actionSettings:bulkDelete',
@ -27,6 +28,16 @@ export const bulkDeleteActionSettings = new SchemaSettings({
return buttonEditorProps; return buttonEditorProps;
}, },
}, },
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{ {
name: 'secondConFirm', name: 'secondConFirm',
Component: SecondConFirm, Component: SecondConFirm,

View File

@ -32,11 +32,9 @@ export const disassociateActionSettings = new SchemaSettings({
name: 'linkageRules', name: 'linkageRules',
Component: SchemaSettingsLinkageRules, Component: SchemaSettingsLinkageRules,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated();
const { linkageRulesProps } = useSchemaToolbar(); const { linkageRulesProps } = useSchemaToolbar();
return { return {
...linkageRulesProps, ...linkageRulesProps,
collectionName: name,
}; };
}, },
}, },

View File

@ -14,7 +14,7 @@ import { useDesignable } from '../../..';
import { useSchemaToolbar } from '../../../application'; import { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
import { RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; import { RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
import { SchemaSettingsModalItem } from '../../../schema-settings'; import { SchemaSettingsModalItem, SchemaSettingsLinkageRules } from '../../../schema-settings';
function ButtonEditor() { function ButtonEditor() {
const field = useField(); const field = useField();
@ -110,6 +110,17 @@ export const expendableActionSettings = new SchemaSettings({
return buttonEditorProps; return buttonEditorProps;
}, },
}, },
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{ {
name: 'remove', name: 'remove',
sort: 100, sort: 100,

View File

@ -11,10 +11,9 @@ import { useField, useFieldSchema } from '@formily/react';
import _ from 'lodash'; import _ from 'lodash';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCollectionRecord, useDesignable } from '../../../'; import { useDesignable } from '../../../';
import { useSchemaToolbar } from '../../../application'; import { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
import { useCollection_deprecated } from '../../../collection-manager';
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
import { import {
SchemaSettingsLinkageRules, SchemaSettingsLinkageRules,
@ -22,6 +21,7 @@ import {
SchemaSettingAccessControl, SchemaSettingAccessControl,
} from '../../../schema-settings'; } from '../../../schema-settings';
import { useURLAndHTMLSchema } from './useURLAndHTMLSchema'; import { useURLAndHTMLSchema } from './useURLAndHTMLSchema';
import { useDataBlockProps } from '../../../data-source';
export const SchemaSettingsActionLinkItem: FC = () => { export const SchemaSettingsActionLinkItem: FC = () => {
const field = useField(); const field = useField();
@ -94,16 +94,10 @@ export const customizeLinkActionSettings = new SchemaSettings({
{ {
name: 'linkageRules', name: 'linkageRules',
Component: SchemaSettingsLinkageRules, Component: SchemaSettingsLinkageRules,
useVisible() {
const record = useCollectionRecord();
return !_.isEmpty(record?.data);
},
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated();
const { linkageRulesProps } = useSchemaToolbar(); const { linkageRulesProps } = useSchemaToolbar();
return { return {
...linkageRulesProps, ...linkageRulesProps,
collectionName: name,
}; };
}, },
}, },

View File

@ -10,7 +10,7 @@
import { useSchemaToolbar } from '../../../application'; import { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
import { ButtonEditor, RemoveButton, SecondConFirm } from '../../../schema-component/antd/action/Action.Designer'; import { ButtonEditor, RemoveButton, SecondConFirm } from '../../../schema-component/antd/action/Action.Designer';
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
export const refreshActionSettings = new SchemaSettings({ export const refreshActionSettings = new SchemaSettings({
name: 'actionSettings:refresh', name: 'actionSettings:refresh',
items: [ items: [
@ -22,6 +22,17 @@ export const refreshActionSettings = new SchemaSettings({
return buttonEditorProps; return buttonEditorProps;
}, },
}, },
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{ {
name: 'secondConFirm', name: 'secondConFirm',
Component: SecondConFirm, Component: SecondConFirm,

View File

@ -29,6 +29,7 @@ import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks
import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings'; import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings';
import { useParentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider'; import { useParentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
import { useDataBlockProps } from '../../../data-source'; import { useDataBlockProps } from '../../../data-source';
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
const Tree = connect( const Tree = connect(
AntdTree, AntdTree,
@ -149,6 +150,16 @@ export const createSubmitActionSettings = new SchemaSettings({
return buttonEditorProps; return buttonEditorProps;
}, },
}, },
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{ {
name: 'secondConfirmation', name: 'secondConfirmation',
Component: SecondConFirm, Component: SecondConFirm,

View File

@ -46,10 +46,6 @@ export const updateSubmitActionSettings = new SchemaSettings({
collectionName: name, collectionName: name,
}; };
}, },
useVisible() {
const fieldSchema = useFieldSchema();
return !fieldSchema.parent['x-initializer'].includes('bulkEditForm');
},
}, },
{ {
name: 'secondConfirmation', name: 'secondConfirmation',

View File

@ -6,17 +6,12 @@
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { useFieldSchema } from '@formily/react';
import { useSchemaToolbar } from '../../../application'; import { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
import { useCollection_deprecated } from '../../../collection-manager';
import { useCollection } from '../../../data-source';
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
import { SchemaSettingsLinkageRules, SchemaSettingAccessControl } from '../../../schema-settings'; import { SchemaSettingsLinkageRules, SchemaSettingAccessControl } from '../../../schema-settings';
import { useOpenModeContext } from '../../popup/OpenModeProvider'; import { useOpenModeContext } from '../../popup/OpenModeProvider';
import { useCurrentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
export const customizePopupActionSettings = new SchemaSettings({ export const customizePopupActionSettings = new SchemaSettings({
name: 'actionSettings:popup', name: 'actionSettings:popup',
@ -33,18 +28,11 @@ export const customizePopupActionSettings = new SchemaSettings({
name: 'linkageRules', name: 'linkageRules',
Component: SchemaSettingsLinkageRules, Component: SchemaSettingsLinkageRules,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated();
const { linkageRulesProps } = useSchemaToolbar(); const { linkageRulesProps } = useSchemaToolbar();
return { return {
...linkageRulesProps, ...linkageRulesProps,
collectionName: name,
}; };
}, },
useVisible() {
const { collection } = useCurrentPopupRecord() || {};
const currentCollection = useCollection();
return !collection || collection?.name === currentCollection?.name;
},
}, },
{ {
name: 'openMode', name: 'openMode',

View File

@ -35,11 +35,16 @@ test.describe('linkage rules', () => {
// 条件singleLineText 字段的值包含 123 时 // 条件singleLineText 字段的值包含 123 时
await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add condition', { exact: true }).click(); await page.getByText('Add condition', { exact: true }).click();
await page.getByTestId('select-filter-field').click();
await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click();
await page.getByLabel('Linkage rules').getByRole('tabpanel').getByRole('textbox').click();
await page.getByLabel('Linkage rules').getByRole('tabpanel').getByRole('textbox').fill('123');
await page.getByLabel('variable-button').first().click();
await page.getByText('Current form').last().click();
await page.getByText('Current form').last().click();
await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).locator('div').click();
// await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click();
await page.getByTestId('right-filter-field').getByRole('textbox').click();
await page.getByTestId('right-filter-field').getByRole('textbox').fill('123');
await page.getByRole('tabpanel').getByRole('textbox').last().fill('123');
// action禁用 longText 字段 // action禁用 longText 字段
await page.getByText('Add property').click(); await page.getByText('Add property').click();
await page.getByTestId('select-linkage-property-field').click(); await page.getByTestId('select-linkage-property-field').click();
@ -81,7 +86,7 @@ test.describe('linkage rules', () => {
// 修改第一组规则,使其条件中包含一个变量 -------------------------------------------------------------------------- // 修改第一组规则,使其条件中包含一个变量 --------------------------------------------------------------------------
// 当 singleLineText 字段的值包含 longText 字段的值时,禁用 longText 字段 // 当 singleLineText 字段的值包含 longText 字段的值时,禁用 longText 字段
await openLinkageRules(); await openLinkageRules();
await page.getByLabel('variable-button').click(); await page.getByLabel('variable-button').last().click();
await expectSupportedVariables(page, [ await expectSupportedVariables(page, [
'Constant', 'Constant',
'Current user', 'Current user',
@ -136,8 +141,13 @@ test.describe('linkage rules', () => {
.getByText('Add condition', { exact: true }) .getByText('Add condition', { exact: true })
.last() .last()
.click(); .click();
await page.getByRole('button', { name: 'Select field' }).click(); // await page.getByRole('button', { name: 'Select field' }).click();
await page.getByRole('menuitemcheckbox', { name: 'number' }).click();
await page.getByTestId('left-filter-field').getByLabel('variable-button').last().click();
await page.getByText('Current form').last().click();
await page.getByText('Current form').last().click();
await page.getByRole('menuitemcheckbox', { name: 'number' }).locator('div').click();
await page.getByLabel('Linkage rules').getByRole('spinbutton').click(); await page.getByLabel('Linkage rules').getByRole('spinbutton').click();
await page.getByLabel('Linkage rules').getByRole('spinbutton').fill('123'); await page.getByLabel('Linkage rules').getByRole('spinbutton').fill('123');

View File

@ -22,7 +22,7 @@ test.describe('deprecated variables', () => {
await expect(page.getByLabel('variable-tag').getByText('Current record / Nickname')).toBeVisible(); await expect(page.getByLabel('variable-tag').getByText('Current record / Nickname')).toBeVisible();
// 2. 但是变量列表中是禁用状态 // 2. 但是变量列表中是禁用状态
await page.locator('button').filter({ hasText: /^x$/ }).click(); await page.locator('button').filter({ hasText: /^x$/ }).last().click();
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).hover({ position: { x: 40, y: 12 } }); 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('tooltip', { name: 'This variable has been deprecated' })).toBeVisible();
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveClass( await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveClass(
@ -45,11 +45,11 @@ test.describe('deprecated variables', () => {
await page.getByLabel('Linkage rules').getByText('Linkage rules').click(); await page.getByLabel('Linkage rules').getByText('Linkage rules').click();
// 3. 当设置为其它变量后,再次打开,变量列表中的弃用变量不再显示 // 3. 当设置为其它变量后,再次打开,变量列表中的弃用变量不再显示
await page.locator('button').filter({ hasText: /^x$/ }).click(); await page.locator('button').filter({ hasText: /^x$/ }).last().click();
await expect(page.getByRole('menuitemcheckbox', { name: 'Current form right' })).toHaveCount(1); 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: 'Current form right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname')).toBeVisible(); await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname').last()).toBeVisible();
// 清空表达式 // 清空表达式
await page.getByLabel('textbox').clear(); await page.getByLabel('textbox').clear();
await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click();
@ -58,7 +58,7 @@ test.describe('deprecated variables', () => {
await page.getByLabel('block-item-CardItem-users-form').hover(); await page.getByLabel('block-item-CardItem-users-form').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:editForm-users').hover(); await page.getByLabel('designer-schema-settings-CardItem-blockSettings:editForm-users').hover();
await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
await page.locator('button').filter({ hasText: /^x$/ }).click(); await page.locator('button').filter({ hasText: /^x$/ }).last().click();
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toBeHidden(); await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toBeHidden();
// 使下拉菜单消失 // 使下拉菜单消失
await page.getByLabel('Linkage rules').getByText('Linkage rules').click(); await page.getByLabel('Linkage rules').getByText('Linkage rules').click();

View File

@ -86,7 +86,6 @@ test.describe('configure fields', () => {
await page.getByRole('menuitem', { name: 'manyToOne3' }).click(); await page.getByRole('menuitem', { name: 'manyToOne3' }).click();
await page.mouse.move(600, 0); await page.mouse.move(600, 0);
await page.reload(); await page.reload();
await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne1-manyToOne1')).toHaveText( await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne1-manyToOne1')).toHaveText(
`manyToOne1:${record.manyToOne1.id}`, `manyToOne1:${record.manyToOne1.id}`,
); );

View File

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

View File

@ -30,7 +30,6 @@ test('action linkage by row data', async ({ page, mockPage }) => {
// 添加其他你需要的样式属性 // 添加其他你需要的样式属性
}; };
}); });
expect(adminEditActionStyle.opacity).not.toBe('0.1'); expect(adminEditActionStyle.opacity).not.toBe('0.1');
expect(rootEditActionStyle.opacity).not.toBe('1'); expect(rootEditActionStyle.opacity).not.toBe('1');
}); });

View File

@ -316,7 +316,8 @@ test.describe('actions schema settings', () => {
// 添加一个条件ID 等于 1 // 添加一个条件ID 等于 1
await page.getByText('Add condition', { exact: true }).click(); await page.getByText('Add condition', { exact: true }).click();
await page.getByTestId('select-filter-field').click(); await page.getByTestId('left-filter-field').getByLabel('variable-button').click();
await page.getByText('Current record').last().click();
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
await page.getByRole('spinbutton').click(); await page.getByRole('spinbutton').click();
await page.getByRole('spinbutton').fill('1'); await page.getByRole('spinbutton').fill('1');
@ -340,7 +341,8 @@ test.describe('actions schema settings', () => {
// 添加一个条件ID 等于 1 // 添加一个条件ID 等于 1
await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).last().click(); await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).last().click();
await page.getByRole('button', { name: 'Select field' }).click(); await page.getByTestId('left-filter-field').getByLabel('variable-button').last().click();
await page.getByText('Current record').last().click();
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
await page.getByRole('spinbutton').click(); await page.getByRole('spinbutton').click();
await page.getByRole('spinbutton').fill('1'); await page.getByRole('spinbutton').fill('1');
@ -902,7 +904,6 @@ test.describe('actions schema settings', () => {
await page.getByRole('menuitem', { name: 'Submit' }).click(); await page.getByRole('menuitem', { name: 'Submit' }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('designer-schema-settings-CardItem-TableBlockDesigner-treeCollection').hover(); await page.getByLabel('designer-schema-settings-CardItem-TableBlockDesigner-treeCollection').hover();
await page.getByRole('menuitem', { name: 'Tree table' }).click(); await page.getByRole('menuitem', { name: 'Tree table' }).click();
@ -928,6 +929,7 @@ test.describe('actions schema settings', () => {
await page.getByLabel('schema-initializer-Grid-form:').hover(); await page.getByLabel('schema-initializer-Grid-form:').hover();
await page.getByRole('menuitem', { name: 'Parent', exact: true }).click(); await page.getByRole('menuitem', { name: 'Parent', exact: true }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);
await page.reload();
await expect( await expect(
page page
.getByLabel('block-item-CollectionField-') .getByLabel('block-item-CollectionField-')

View File

@ -27,7 +27,8 @@ test.describe('options of Select field in linkage rule', () => {
await page.getByRole('switch', { name: 'On Off' }).click(); await page.getByRole('switch', { name: 'On Off' }).click();
await page.getByRole('button', { name: 'OK' }).click(); await page.getByRole('button', { name: 'OK' }).click();
await page.reload(); await page.reload();
await expect(page.getByRole('option', { name: 'option2' })).toBeVisible(); await page.getByLabel('block-item-CollectionField-').click();
await expect(page.getByRole('option', { name: 'option2' }).last()).toBeVisible();
await expect(page.getByRole('option', { name: 'option3' })).toBeVisible(); await expect(page.getByRole('option', { name: 'option3' })).toBeVisible();
}); });
}); });

View File

@ -215,7 +215,7 @@ test.describe('where to open a popup and what can be added to it', () => {
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible(); await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).click();
await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1); await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
await page.getByRole('menuitem', { name: 'Associated records' }).hover(); await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'One to many' }).click(); await page.getByRole('menuitem', { name: 'One to many' }).click();
@ -282,7 +282,7 @@ test.describe('where to open a popup and what can be added to it', () => {
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible(); await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).click();
await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1); await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
await page.getByRole('menuitem', { name: 'Associated records' }).hover(); await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'One to many' }).click(); await page.getByRole('menuitem', { name: 'One to many' }).click();

View File

@ -18,7 +18,7 @@ test.describe('variables', () => {
await page.getByLabel('action-Action.Link-View-view-').hover(); await page.getByLabel('action-Action.Link-View-view-').hover();
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover(); await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover();
await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
await page.getByLabel('variable-button').click(); await page.getByTestId('left-filter-field').getByLabel('variable-button').click();
// 2. 断言应该显示的变量 // 2. 断言应该显示的变量
['Constant', 'Current user', 'Current role', 'API token', 'Date variables', 'Current record'].forEach( ['Constant', 'Current user', 'Current role', 'API token', 'Date variables', 'Current record'].forEach(

View File

@ -22,14 +22,14 @@ test.describe('variable: Current Record', () => {
await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add condition', { exact: true }).click(); await page.getByText('Add condition', { exact: true }).click();
await page.getByLabel('variable-button').click(); await page.getByLabel('variable-button').first().click();
// 当前表单中应该包含 “Nickname” 字段 // 当前表单中应该包含 “Nickname” 字段
await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
// 当前对象中应该包含 “Role UID” 字段 // 当前对象中应该包含 “Role UID” 字段
await page.getByLabel('variable-button').click(); await page.getByLabel('variable-button').first().click();
await page.getByText('Current object').click(); await page.getByText('Current object').click();
await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Role UID' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Role UID' }).click();
@ -43,12 +43,12 @@ test.describe('variable: Current Record', () => {
await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add condition', { exact: true }).click(); await page.getByText('Add condition', { exact: true }).click();
await page.getByLabel('variable-button').click(); await page.getByLabel('variable-button').first().click();
// 当前记录中应该包含 “Nickname” 字段 // 当前记录中应该包含 “Nickname” 字段
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
await page.getByLabel('variable-button').click(); await page.getByLabel('variable-button').first().click();
// 当前对象中应该包含 “Role UID” 字段 // 当前对象中应该包含 “Role UID” 字段
await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click();

View File

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

View File

@ -710,27 +710,11 @@ export const InternalAdminLayout = () => {
); );
}; };
function getDefaultPageUid(routes: NocoBaseDesktopRoute[]) {
// Find the first route of type "page"
for (const route of routes) {
if (route.type === NocoBaseDesktopRouteType.page) {
return route.schemaUid;
}
if (route.children?.length) {
const result = getDefaultPageUid(route.children);
if (result) {
return result;
}
}
}
}
const NavigateToDefaultPage: FC = (props) => { const NavigateToDefaultPage: FC = (props) => {
const { allAccessRoutes } = useAllAccessDesktopRoutes(); const { allAccessRoutes } = useAllAccessDesktopRoutes();
const location = useLocationNoUpdate(); const location = useLocationNoUpdate();
const defaultPageUid = getDefaultPageUid(allAccessRoutes); const defaultPageUid = findFirstPageRoute(allAccessRoutes)?.schemaUid;
return ( return (
<> <>
@ -962,16 +946,17 @@ function findRouteById(id: string, treeArray: any[]) {
return null; return null;
} }
function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) { export function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) {
if (!routes) return; if (!routes) return;
for (const route of routes) { for (const route of routes.filter((item) => !item.hideInMenu)) {
if (route.type === NocoBaseDesktopRouteType.page) { if (route.type === NocoBaseDesktopRouteType.page) {
return route; return route;
} }
if (route.children?.length) { if (route.type === NocoBaseDesktopRouteType.group && route.children?.length) {
return findFirstPageRoute(route.children); const result = findFirstPageRoute(route.children);
if (result) return result;
} }
} }
} }

View File

@ -49,6 +49,7 @@ import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
import { ActionContextProps, ActionProps, ComposedAction } from './types'; import { ActionContextProps, ActionProps, ComposedAction } from './types';
import { linkageAction, setInitialActionState } from './utils'; import { linkageAction, setInitialActionState } from './utils';
import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant'; import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant';
import { BlockContext } from '../../../block-provider/BlockProvider';
// 这个要放到最下面,否则会导致前端单测失败 // 这个要放到最下面,否则会导致前端单测失败
import { useApp } from '../../../application'; import { useApp } from '../../../application';
@ -96,7 +97,9 @@ export const Action: ComposedAction = withDynamicSchemaProps(
const { designable } = useDesignable(); const { designable } = useDesignable();
const tarComponent = useComponent(component) || component; const tarComponent = useComponent(component) || component;
const variables = useVariables(); const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: { values: recordData, readPretty: false } as any }); const localVariables = useLocalVariables({
currentForm: { values: recordData, readPretty: false } as any,
});
const { visibleWithURL, setVisibleWithURL } = usePopupUtils(); const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
const { setSubmitted } = useActionContext(); const { setSubmitted } = useActionContext();
const { getAriaLabel } = useGetAriaLabelOfAction(title); const { getAriaLabel } = useGetAriaLabelOfAction(title);
@ -120,6 +123,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
condition: v.condition, condition: v.condition,
variables, variables,
localVariables, localVariables,
conditionType: v.conditionType,
}, },
app.jsonLogic, app.jsonLogic,
); );
@ -155,6 +159,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
}, [onClick, fieldSchema, getAllDataBlocks]); }, [onClick, fieldSchema, getAllDataBlocks]);
return ( return (
<BlockContext.Provider value={{ name: 'action' }}>
<InternalAction <InternalAction
containerRefKey={containerRefKey} containerRefKey={containerRefKey}
fieldSchema={fieldSchema} fieldSchema={fieldSchema}
@ -167,7 +172,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
className={className} className={className}
type={props.type} type={props.type}
Designer={Designer} Designer={Designer}
onClick={handleClick} onClick={onClick}
confirm={confirm} confirm={confirm}
confirmTitle={confirmTitle} confirmTitle={confirmTitle}
popover={popover} popover={popover}
@ -185,6 +190,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
actionCallback={actionCallback} actionCallback={actionCallback}
{...others} {...others}
/> />
</BlockContext.Provider>
); );
}), }),
{ displayName: 'Action' }, { displayName: 'Action' },

View File

@ -32,7 +32,7 @@ export const useGetAriaLabelOfAction = (title: string) => {
let { name: blockName } = useBlockContext() || {}; let { name: blockName } = useBlockContext() || {};
const actionTitle = title || compile(fieldSchema.title); const actionTitle = title || compile(fieldSchema.title);
collectionName = collectionName ? `-${collectionName}` : ''; collectionName = collectionName ? `-${collectionName}` : '';
blockName = blockName ? `-${blockName}` : ''; blockName = blockName && blockName !== 'action' ? `-${blockName}` : '';
action = action ? `-${action}` : ''; action = action ? `-${action}` : '';
recordName = recordName ? `-${recordName}` : ''; recordName = recordName ? `-${recordName}` : '';

View File

@ -87,12 +87,14 @@ export const linkageAction = async (
condition, condition,
variables, variables,
localVariables, localVariables,
conditionType,
}: { }: {
operator; operator;
field; field;
condition; condition;
variables: VariablesContextType; variables: VariablesContextType;
localVariables: VariableOption[]; localVariables: VariableOption[];
conditionType: 'advanced' | 'basic';
}, },
jsonLogic: any, jsonLogic: any,
) => { ) => {
@ -101,7 +103,7 @@ export const linkageAction = async (
switch (operator) { switch (operator) {
case ActionType.Visible: case ActionType.Visible:
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
displayResult.push(operator); displayResult.push(operator);
field.data = field.data || {}; field.data = field.data || {};
field.data.hidden = false; field.data.hidden = false;
@ -113,7 +115,7 @@ export const linkageAction = async (
field.display = last(displayResult); field.display = last(displayResult);
break; break;
case ActionType.Hidden: case ActionType.Hidden:
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
field.data = field.data || {}; field.data = field.data || {};
field.data.hidden = true; field.data.hidden = true;
} else { } else {
@ -122,7 +124,7 @@ export const linkageAction = async (
} }
break; break;
case ActionType.Disabled: case ActionType.Disabled:
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
disableResult.push(true); disableResult.push(true);
} }
field.stateOfLinkageRules = { field.stateOfLinkageRules = {
@ -133,7 +135,7 @@ export const linkageAction = async (
field.componentProps['disabled'] = last(disableResult); field.componentProps['disabled'] = last(disableResult);
break; break;
case ActionType.Active: case ActionType.Active:
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
disableResult.push(false); disableResult.push(false);
} else { } else {
disableResult.push(!!field.componentProps?.['disabled']); disableResult.push(!!field.componentProps?.['disabled']);

View File

@ -27,7 +27,7 @@ export const useGetAriaLabelOfBlockItem = (name?: string) => {
let { name: blockName } = useBlockContext() || {}; let { name: blockName } = useBlockContext() || {};
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let { name: collectionName, getField } = useCollection_deprecated(); let { name: collectionName, getField } = useCollection_deprecated();
blockName = name || blockName; blockName = name || (blockName !== 'action' ? blockName : '');
const title = compile(fieldSchema['title']) || compile(getField(fieldSchema.name)?.uiSchema?.title); const title = compile(fieldSchema['title']) || compile(getField(fieldSchema.name)?.uiSchema?.title);

View File

@ -50,7 +50,7 @@ describe('CollectionSelect', () => {
> >
<div <div
aria-label="block-item-demo title" aria-label="block-item-demo title"
class="nb-block-item nb-form-item css-9qorhu ant-nb-block-item css-dev-only-do-not-override-1rquknz" class="nb-block-item nb-form-item css-1elzyjx ant-nb-block-item css-dev-only-do-not-override-1rquknz"
role="button" role="button"
> >
<div <div
@ -191,7 +191,7 @@ describe('CollectionSelect', () => {
> >
<div <div
aria-label="block-item-demo title" aria-label="block-item-demo title"
class="nb-block-item nb-form-item css-9qorhu ant-nb-block-item css-dev-only-do-not-override-1rquknz" class="nb-block-item nb-form-item css-1elzyjx ant-nb-block-item css-dev-only-do-not-override-1rquknz"
role="button" role="button"
> >
<div <div

View File

@ -39,15 +39,13 @@ const formItemWrapCss = css`
.ant-description-textarea img { .ant-description-textarea img {
max-width: 100%; max-width: 100%;
} }
&.ant-formily-item-layout-horizontal.ant-formily-item-label-wrap { &.ant-formily-item-layout-vertical .ant-formily-item-label {
.ant-formily-item-label {
display: inline; display: inline;
padding-right: 5px; .ant-formily-item-label-tooltip-icon {
.ant-formily-item-label-tooltip-icon,
.ant-formily-item-label-content {
display: inline; display: inline;
} }
.ant-formily-item-label-content {
display: inline;
} }
} }
`; `;
@ -89,7 +87,7 @@ export const FormItem: any = withDynamicSchemaProps(
}} }}
/> />
) : ( ) : (
t(field.description, { ns: NAMESPACE_UI_SCHEMA }) field.description
); );
} }
}, [field.description]); }, [field.description]);
@ -114,9 +112,6 @@ export const FormItem: any = withDynamicSchemaProps(
? '100% !important' ? '100% !important'
: null}; : null};
} }
.ant-formily-item-control {
padding: ${showTitle === false ? '5px' : '0px'};
}
`, `,
)} )}
> >

View File

@ -20,6 +20,7 @@ import { useAttach, useComponent } from '../..';
import { useApp } from '../../../application'; import { useApp } from '../../../application';
import { getCardItemSchema } from '../../../block-provider'; import { getCardItemSchema } from '../../../block-provider';
import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider'; import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider';
import { useDataBlockProps } from '../../../data-source';
import { useDataBlockRequest } from '../../../data-source/data-block/DataBlockRequestProvider'; import { useDataBlockRequest } from '../../../data-source/data-block/DataBlockRequestProvider';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField'; import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
@ -150,12 +151,15 @@ const WithForm = (props: WithFormProps) => {
const linkageRules: any[] = const linkageRules: any[] =
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || []; (getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
// 关闭弹窗之前,如果有未保存的数据,是否要二次确认
const { confirmBeforeClose = true } = useDataBlockProps() || ({} as any);
useEffect(() => { useEffect(() => {
const id = uid(); const id = uid();
form.addEffects(id, () => { form.addEffects(id, () => {
onFormInputChange(() => { onFormInputChange(() => {
setFormValueChanged?.(true); setFormValueChanged?.(confirmBeforeClose);
}); });
}); });
@ -166,7 +170,7 @@ const WithForm = (props: WithFormProps) => {
return () => { return () => {
form.removeEffects(id); form.removeEffects(id);
}; };
}, [form, props.disabled, setFormValueChanged]); }, [form, props.disabled, setFormValueChanged, confirmBeforeClose]);
useEffect(() => { useEffect(() => {
if (loading) { if (loading) {
@ -219,17 +223,19 @@ const WithForm = (props: WithFormProps) => {
const WithoutForm = (props) => { const WithoutForm = (props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { setFormValueChanged } = useActionContext(); const { setFormValueChanged } = useActionContext();
// 关闭弹窗之前,如果有未保存的数据,是否要二次确认
const { confirmBeforeClose = true } = useDataBlockProps() || ({} as any);
const form = useMemo( const form = useMemo(
() => () =>
createForm({ createForm({
disabled: props.disabled, disabled: props.disabled,
effects() { effects() {
onFormInputChange((form) => { onFormInputChange((form) => {
setFormValueChanged?.(true); setFormValueChanged?.(confirmBeforeClose);
}); });
}, },
}), }),
[], [confirmBeforeClose],
); );
return fieldSchema['x-decorator'] === 'FormV2' ? ( return fieldSchema['x-decorator'] === 'FormV2' ? (
<FormDecorator form={form} {...props} /> <FormDecorator form={form} {...props} />

View File

@ -66,4 +66,5 @@ export * from './unix-timestamp';
export * from './upload'; export * from './upload';
export * from './variable'; export * from './variable';
export * from './form-drawer'; export * from './form-drawer';
export * from './linkageFilter';
import './index.less'; import './index.less';

View File

@ -0,0 +1,127 @@
/**
* 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 { createForm, onFieldValueChange } from '@formily/core';
import { FieldContext, FormContext } from '@formily/react';
import { merge } from '@formily/shared';
import React, { useCallback, useContext, useMemo } from 'react';
import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
import { SchemaComponent } from '../../core';
import { FilterContext } from './context';
import { VariableInput, getShouldChange } from '../../../schema-settings/VariableInput/VariableInput';
import { useCollectionRecordData } from '../../../data-source';
import { useLocalVariables, useVariables } from '../../../variables';
import { useCollectionManager_deprecated } from '../../../collection-manager';
export interface DynamicComponentProps {
value: any;
/**
* `Filter`
*/
collectionField: CollectionFieldOptions_deprecated;
onChange: (value: any) => void;
renderSchemaComponent: () => React.JSX.Element;
}
interface Props {
value: any;
collectionField?: CollectionFieldOptions_deprecated;
onChange: (value: any) => void;
style?: React.CSSProperties;
componentProps?: any;
schema?: any;
setScopes?: any;
testid?: string;
nullable?: boolean;
constantAbel?: boolean;
changeOnSelect?: boolean;
readOnly?: boolean;
}
export const DynamicComponent = (props: Props) => {
const { setScopes, nullable, constantAbel, changeOnSelect, readOnly = false } = props;
const { disabled } = useContext(FilterContext) || {};
const record = useCollectionRecordData();
const variables = useVariables();
const localVariables = useLocalVariables();
const { getAllCollectionsInheritChain } = useCollectionManager_deprecated();
const { collectionField } = props;
const component = useCallback((props: DynamicComponentProps) => {
return (
<VariableInput
{...props}
form={form}
record={record}
setScopes={setScopes}
nullable={nullable}
constantAbel={constantAbel}
changeOnSelect={changeOnSelect}
shouldChange={getShouldChange({
collectionField,
variables,
localVariables,
getAllCollectionsInheritChain,
})}
/>
);
}, []);
const form = useMemo(() => {
return createForm({
values: {
value: props.value,
},
effects() {
onFieldValueChange('value', (field) => {
props?.onChange?.(field.value);
});
},
disabled,
});
}, [JSON.stringify(props.value), props.schema]);
const renderSchemaComponent: any = useCallback(() => {
const componentProps = merge(props?.schema?.['x-component-props'] || {}, props.componentProps || {});
return (
<FieldContext.Provider value={null}>
<SchemaComponent
schema={{
'x-component': 'Input',
...props.schema,
'x-component-props': merge(componentProps, {
style: {
minWidth: 150,
...props.style,
},
utc: false,
readOnly: readOnly,
}),
name: 'value',
'x-read-pretty': false,
'x-validator': undefined,
'x-decorator': undefined,
}}
/>
</FieldContext.Provider>
);
}, [props.schema]);
return (
<FormContext.Provider value={form}>
<div data-testid={props.testid}>
{React.createElement<DynamicComponentProps>(component, {
value: props.value,
collectionField: props.collectionField,
onChange: props?.onChange,
renderSchemaComponent,
})}
</div>
</FormContext.Provider>
);
};
export const FilterDynamicComponent = DynamicComponent;

View File

@ -0,0 +1,124 @@
/**
* 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 { CloseCircleOutlined } from '@ant-design/icons';
import { ObjectField as ObjectFieldModel } from '@formily/core';
import { ArrayField, connect, useField } from '@formily/react';
import { Select, Space } from 'antd';
import React, { useContext } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useToken } from '../__builtins__';
import { FilterItems } from './FilterItems';
import { FilterLogicContext, RemoveConditionContext } from './context';
export const FilterGroup = connect((props) => {
const { bordered = true, disabled } = props;
const field = useField<ObjectFieldModel>();
const remove = useContext(RemoveConditionContext);
const { t } = useTranslation();
const { token } = useToken();
const keys = Object.keys(field.value || {});
const logic = keys.includes('$or') ? '$or' : '$and';
const setLogic = (value) => {
const obj = field.value || {};
field.value = {
[value]: [...(obj[logic] || [])],
};
};
const mergedDisabled = disabled || field.disabled;
return (
<FilterLogicContext.Provider value={logic}>
<div
style={
bordered
? {
position: 'relative',
border: `1px dashed ${token.colorBorder}`,
padding: token.paddingSM,
marginBottom: token.marginXS,
}
: {
position: 'relative',
marginBottom: token.marginXS,
}
}
>
{remove && !mergedDisabled && (
<a role="button" aria-label="icon-close">
<CloseCircleOutlined
style={{
position: 'absolute',
right: 10,
top: 10,
color: '#bfbfbf',
}}
onClick={() => remove()}
/>
</a>
)}
<div style={{ marginBottom: 8, color: token.colorText }}>
<Trans>
{'Meet '}
<Select
// @ts-ignore
role="button"
data-testid="filter-select-all-or-any"
style={{ width: 'auto' }}
value={logic}
onChange={(value) => {
setLogic(value);
}}
>
<Select.Option value={'$and'}>All</Select.Option>
<Select.Option value={'$or'}>Any</Select.Option>
</Select>
{' conditions in the group'}
</Trans>
</div>
<div>
<ArrayField name={`${logic}`} component={[FilterItems]} disabled={mergedDisabled} />
</div>
{!mergedDisabled && (
<Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
<a
onClick={() => {
const value = field.value || {};
const items = value[logic] || [];
items.push({});
field.value = {
[logic]: items,
};
field.initialValue = {
[logic]: items,
};
}}
>
{t('Add condition')}
</a>
<a
onClick={() => {
const value = field.value || {};
const items = value[logic] || [];
items.push({
$and: [{}],
});
field.value = {
[logic]: items,
};
}}
>
{t('Add condition group')}
</a>
</Space>
)}
</div>
</FilterLogicContext.Provider>
);
});

View File

@ -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 { ArrayField as ArrayFieldModel } from '@formily/core';
import { ObjectField, observer, useField } from '@formily/react';
import React from 'react';
import { FilterGroup } from './FilterGroup';
import { LinkageFilterItem } from './LinkageFilterItem';
import { RemoveConditionContext } from './context';
export const FilterItems = observer(
(props) => {
const field = useField<ArrayFieldModel>();
return (
<div>
{field?.value?.filter(Boolean).map((item, index) => {
return (
<RemoveConditionContext.Provider key={index} value={() => field.remove(index)}>
<ObjectField name={index} component={[item.$and || item.$or ? FilterGroup : LinkageFilterItem]} />
</RemoveConditionContext.Provider>
);
})}
</div>
);
},
{ displayName: 'FilterItems' },
);

View File

@ -0,0 +1,65 @@
/**
* 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 { ObjectField as ObjectFieldModel } from '@formily/core';
import { observer, useField, useFieldSchema } from '@formily/react';
import React, { useEffect, useState } from 'react';
import { UseRequestOptions, useRequest } from '../../../api-client';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
import { useProps } from '../../hooks/useProps';
import { FilterGroup } from './FilterGroup';
import { FilterContext } from './context';
const useDef = (options: UseRequestOptions) => {
const field = useField<ObjectFieldModel>();
return useRequest(() => Promise.resolve({ data: field.dataSource }), options);
};
export const LinkageFilter: any = withDynamicSchemaProps(
observer((props: any) => {
const { useDataSource = useDef } = props;
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { dynamicComponent, className, collectionName } = useProps(props);
const [scopes, setScopes] = useState([]);
const field = useField<ObjectFieldModel>();
const fieldSchema: any = useFieldSchema();
useDataSource({
onSuccess(data) {
field.dataSource = data?.data || [];
},
});
useEffect(() => {
if (fieldSchema.defaultValue) {
field.initialValue = fieldSchema.defaultValue;
}
}, [fieldSchema.defaultValue]);
return (
<div className={className}>
<FilterContext.Provider
value={{
field,
fieldSchema,
dynamicComponent,
disabled: props.disabled,
collectionName,
scopes,
setScopes,
}}
>
<FilterGroup {...props} bordered={false} />
</FilterContext.Provider>
</div>
);
}),
{ displayName: 'LinkageFilter' },
);

View File

@ -0,0 +1,78 @@
/**
* 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 { CloseCircleOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { observer } from '@formily/react';
import { Select, Space } from 'antd';
import React, { useCallback, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useCompile } from '../../hooks';
import { DynamicComponent } from './DynamicComponent';
import { RemoveConditionContext } from './context';
import { useValues } from './useValues';
import { FilterContext } from './context';
export const LinkageFilterItem = observer(
(props: any) => {
const { t } = useTranslation();
const compile = useCompile();
const remove = useContext(RemoveConditionContext);
const { setScopes } = useContext(FilterContext) || {};
const { schema, operators, operator, setOperator, rightVar, leftVar, setLeftValue, setRightValue } = useValues();
const style = useMemo(() => ({ marginBottom: 8 }), []);
const onOperatorsChange = useCallback(
(value) => {
setOperator(value);
},
[setOperator],
);
const removeStyle = useMemo(() => ({ color: '#bfbfbf' }), []);
return (
// 添加 nc-filter-item 类名是为了帮助编写测试时更容易选中该元素
<div style={style} className="nc-filter-item">
<Space wrap>
<DynamicComponent
value={leftVar}
onChange={setLeftValue}
setScopes={setScopes}
testid="left-filter-field"
nullable={false}
constantAbel={false}
changeOnSelect={false}
readOnly={true}
/>
<Select
// @ts-ignore
role="button"
data-testid="select-filter-operator"
className={css`
min-width: 110px;
`}
popupMatchSelectWidth={false}
value={operator?.value}
options={compile(operators)}
onChange={onOperatorsChange}
placeholder={t('Comparision')}
/>
{!operator?.noValue ? (
<DynamicComponent value={rightVar} schema={schema} onChange={setRightValue} testid="right-filter-field" />
) : null}
{!props.disabled && (
<a role="button" aria-label="icon-close">
<CloseCircleOutlined onClick={remove} style={removeStyle} />
</a>
)}
</Space>
</div>
);
},
{ displayName: 'FilterItem' },
);

View File

@ -0,0 +1,30 @@
/**
* 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 { ObjectField } from '@formily/core';
import { Schema } from '@formily/react';
import { ComponentType, createContext } from 'react';
import { DynamicComponentProps } from './DynamicComponent';
export interface FilterContextProps {
field?: ObjectField & { collectionName?: string };
fieldSchema?: Schema;
dynamicComponent?: ComponentType<DynamicComponentProps>;
disabled?: boolean;
collectionName?: string;
scopes?: any[];
setScopes?: any;
}
export const RemoveConditionContext = createContext(null);
RemoveConditionContext.displayName = 'RemoveConditionContext';
export const FilterContext = createContext<FilterContextProps>(null);
FilterContext.displayName = 'FilterContext';
export const FilterLogicContext = createContext(null);
FilterLogicContext.displayName = 'FilterLogicContext';

View File

@ -0,0 +1,148 @@
import React, { useMemo } from 'react';
import { useField, observer, ISchema } from '@formily/react';
import { FilterActionProps, useRequest, SchemaComponent, Plugin } from '@nocobase/client';
import { ArrayCollapse, FormLayout } from '@formily/antd-v5';
import { css } from '@emotion/css';
import { mockApp } from '@nocobase/client/demo-utils';
const ShowFilterData = observer(({ children }) => {
const field = useField<any>();
return (
<>
<pre>{JSON.stringify(field.value, null, 2)}</pre>
{children}
</>
);
});
const useFilterActionProps = (): FilterActionProps => {
const field = useField<any>();
const { run } = useRequest({ url: 'test' }, { manual: true });
return {
onSubmit: async (values) => {
console.log('onSubmit', values);
// request api
run(values);
field.setValue(values);
},
onReset: (values) => {
console.log('onReset', values);
},
};
};
const schema = {
type: 'object',
'x-decorator': 'FormV2',
'x-component': 'ShowFormData',
properties: {
rules: {
type: 'array',
default: [{}],
'x-component': 'ArrayCollapse',
'x-decorator': 'FormItem',
'x-component-props': {
accordion: true,
},
items: {
type: 'object',
'x-component': 'ArrayCollapse.CollapsePanel',
'x-component-props': {
// extra: 'linkage rule',
},
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
labelStyle: {
marginTop: '6px',
},
labelCol: 8,
wrapperCol: 16,
},
properties: {
conditions: {
'x-component': 'h4',
'x-content': '{{ t("Condition") }}',
},
condition: {
'x-component': 'LinkageFilter',
'x-use-component-props': () => {
return {
// options,
className: css`
position: relative;
width: 100%;
margin-left: 10px;
`,
};
},
},
},
},
remove: {
type: 'void',
'x-component': 'ArrayCollapse.Remove',
},
moveUp: {
type: 'void',
'x-component': 'ArrayCollapse.MoveUp',
},
moveDown: {
type: 'void',
'x-component': 'ArrayCollapse.MoveDown',
},
copy: {
type: 'void',
'x-component': 'ArrayCollapse.Copy',
},
},
},
properties: {
add: {
type: 'void',
title: '{{ t("Add linkage rule") }}',
'x-component': 'ArrayCollapse.Addition',
'x-reactions': {
dependencies: ['rules'],
fulfill: {
state: {
// disabled: '{{$deps[0].length >= 3}}',
},
},
},
},
},
},
},
};
const Demo = () => {
return (
<SchemaComponent
schema={schema}
components={{ ShowFilterData, ArrayCollapse, FormLayout }}
scope={{ useFilterActionProps }}
/>
);
};
class DemoPlugin extends Plugin {
async load() {
this.app.router.add('root', { path: '/', Component: Demo });
}
}
const app = mockApp({
plugins: [DemoPlugin],
apis: {
test: { data: { data: 'ok' } },
},
});
export default app.getRootComponent();

View File

@ -0,0 +1,81 @@
import React from 'react';
import { useField, observer } from '@formily/react';
import { FilterActionProps, ISchema, useDataBlockRequest } from '@nocobase/client';
import { mockApp } from '@nocobase/client/demo-utils';
import { SchemaComponent, Plugin } from '@nocobase/client';
const ShowFilterData = observer(({ children }) => {
const field = useField<any>();
return (
<>
<pre>{JSON.stringify(field.value, null, 2)}</pre>
{children}
</>
);
});
const useFilterActionProps = (): FilterActionProps => {
const field = useField<any>();
const { run } = useDataBlockRequest(); // replace `useRequest`
return {
onSubmit: async (values) => {
console.log('onSubmit', values);
// request api
run(values);
field.setValue(values);
},
onReset: (values) => {
console.log('onReset', values);
},
};
};
const schema: ISchema = {
type: 'void',
name: 'root',
'x-decorator': 'DataBlockProvider',
'x-decorator-props': {
collection: 'users',
action: 'list',
},
properties: {
test: {
name: 'filter',
type: 'object',
title: 'Filter',
'x-decorator': 'ShowFilterData',
'x-component': 'Filter.Action',
'x-use-component-props': 'useFilterActionProps',
},
},
};
const Demo = () => {
return (
<SchemaComponent
schema={schema}
components={{ ShowFilterData }}
scope={{
useFilterActionProps,
}}
/>
);
};
class DemoPlugin extends Plugin {
async load() {
this.app.router.add('root', { path: '/', Component: Demo });
}
}
const app = mockApp({
plugins: [DemoPlugin],
apis: {
test: { data: { data: 'ok' } },
},
});
export default app.getRootComponent();

View File

@ -0,0 +1,17 @@
# LinkageFilter
A component used for filtering data, commonly used to filter data in blocks.
```ts
type FilterActionProps<T = {}> = ActionProps & {
options?: any[];
form?: Form;
onSubmit?: (values: T) => void;
onReset?: (values: T) => void;
}
```
### Basic Usage
<code src="./demos/new-demos/basic.tsx"></code>

View File

@ -0,0 +1,18 @@
# LinkageFilter
用于前端联动规则中,用作条件配置
```ts
type FilterActionProps<T = {}> = ActionProps & {
options?: any[];
form?: Form;
onSubmit?: (values: T) => void;
onReset?: (values: T) => void;
}
```
### Basic Usage
左侧支持变量,操作符、和右侧变量组件跟随左侧变量联动
<code src="./demos/new-demos/basic.tsx"></code>

View File

@ -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 './LinkageFilter';

View File

@ -0,0 +1,34 @@
/**
* 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 { useFieldSchema } from '@formily/react';
import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
import { useMemo } from 'react';
/**
*
* @returns
*/
export const useOperatorList = (): any[] => {
const schema = useFieldSchema();
const { name } = useCollection_deprecated();
const { getCollectionFields, getInterface } = useCollectionManager_deprecated();
const res = useMemo(() => {
const fieldInterface = schema['x-designer-props']?.interface;
const collectionFields = getCollectionFields(name);
if (fieldInterface) {
return getInterface(fieldInterface)?.filterable?.operators || [];
}
const field = collectionFields.find((item) => item.name === schema.name);
const ops = getInterface(field?.interface)?.filterable?.operators || [];
return ops.filter((o) => typeof o.visible !== 'function' || o.visible(field));
}, [schema.name]);
return res;
};

View File

@ -0,0 +1,141 @@
/**
* 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 { useField } from '@formily/react';
import { merge } from '@formily/shared';
import { cloneDeep, last, uniqBy } from 'lodash';
import { useCallback, useContext, useEffect } from 'react';
import { FilterContext } from './context';
interface UseValuesReturn {
fields: any[];
collectionField: any;
dataIndex: string[];
operators: any[];
operator: any;
schema: any;
value: any;
setDataIndex: (dataIndex: string[]) => void;
setOperator: (operatorValue: string) => void;
setRightValue: (value: any) => void;
setLeftValue: (value: any) => void;
leftVar: any;
rightVar: any;
}
const findOption = (str, options) => {
if (!str) return null;
const match = str.match(/\{\{\$(.*?)\}\}/);
if (!match) return null;
const [firstKey, ...subKeys] = match[1].split('.'); // 拆分层级
const keys = [`$${firstKey}`, ...subKeys]; // 第一层保留 `$`,后续不带 `$`
let currentOptions = options;
let option = null;
for (const key of keys) {
option = currentOptions.find((opt) => opt.value === key);
if (!option) return null;
// 进入下一层 children 查找
if (Array.isArray(option.children) || option.isLeaf === false) {
currentOptions = option.children;
} else {
return option; // 没有 children 直接返回
}
}
return option;
};
const operators = [
{ label: '{{t("is empty")}}', value: '$empty', noValue: true },
{ label: '{{t("is not empty")}}', value: '$notEmpty', noValue: true },
];
export const useValues = (): UseValuesReturn => {
const field = useField<any>();
const { scopes } = useContext(FilterContext) || {};
const { op, leftVar, rightVar } = field.value || {};
const data2value = useCallback(() => {
field.value = field.data.leftVar
? {
op: field.data.operator?.value,
leftVar: field.data.leftVar,
rightVar: field.data?.rightVar,
}
: {};
}, [field]);
const value2data = () => {
/**
* scopes
*/
setTimeout(() => {
const option = findOption(leftVar, scopes);
field.data = field.data || {};
if (!field.value) {
return;
}
const combOperators = uniqBy([...(field.data.operators || []), ...(option?.operators || [])], 'value');
field.data.operators = combOperators.length ? combOperators : operators;
field.data.leftVar = leftVar;
field.data.rightVar = rightVar;
const operator = combOperators?.find((v) => v.value === op);
field.data.operator = field.data.operator || operator;
const s1 = cloneDeep(option?.schema);
const s2 = cloneDeep(operator?.schema);
field.data.schema = field.data?.schema || merge(s1, s2);
}, 100);
};
useEffect(value2data, [field.value, scopes]);
const setLeftValue = useCallback(
(leftVar, paths) => {
const option: any = last(paths);
field.data = field.data || {};
field.data.operators = option?.operators || operators;
const operator = field.data.operators?.[0];
field.data.operator = operator;
const s1 = cloneDeep(option?.schema);
const s2 = cloneDeep(operator?.schema);
field.data.schema = merge(s1, s2);
field.data.leftVar = leftVar;
field.data.rightVar = operator?.noValue ? operator.default || true : undefined;
data2value();
},
[data2value, field],
);
const setOperator = useCallback(
(operatorValue) => {
const operator = field.data?.operators?.find?.((item) => item.value === operatorValue);
field.data.operator = operator;
const s1 = cloneDeep(field.data.schema);
const s2 = cloneDeep(operator?.schema);
field.data.schema = merge(s1, s2);
field.data.value = operator.noValue ? operator.default || true : undefined;
data2value();
},
[data2value, field.data],
);
const setRightValue = useCallback(
(rightVar) => {
field.data.rightVar = rightVar;
data2value();
},
[data2value, field.data],
);
return {
...(field?.data || {}),
setLeftValue,
setOperator,
setRightValue,
};
};

View File

@ -48,6 +48,7 @@ export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
CustomDropdownRender?: (v: any) => any; CustomDropdownRender?: (v: any) => any;
optionFilter?: (option: any) => boolean; optionFilter?: (option: any) => boolean;
toOptionsItem?: (data) => any; toOptionsItem?: (data) => any;
onSuccess?: (data) => any;
}; };
const InternalRemoteSelect = withDynamicSchemaProps( const InternalRemoteSelect = withDynamicSchemaProps(
@ -68,6 +69,7 @@ const InternalRemoteSelect = withDynamicSchemaProps(
dataSource: propsDataSource, dataSource: propsDataSource,
toOptionsItem = (value) => value, toOptionsItem = (value) => value,
popupMatchSelectWidth = false, popupMatchSelectWidth = false,
onSuccess,
...others ...others
} = props; } = props;
const dataSource = useDataSourceKey(); const dataSource = useDataSourceKey();
@ -178,6 +180,7 @@ const InternalRemoteSelect = withDynamicSchemaProps(
{ {
manual, manual,
debounceWait: wait, debounceWait: wait,
onSuccess,
...(service.defaultParams ? { defaultParams: [service.defaultParams] } : {}), ...(service.defaultParams ? { defaultParams: [service.defaultParams] } : {}),
}, },
); );

View File

@ -18,6 +18,8 @@ import {
useDesigner, useDesigner,
useFlag, useFlag,
useSchemaComponentContext, useSchemaComponentContext,
BlockContext,
useBlockContext,
} from '../../../'; } from '../../../';
import { useToken } from '../__builtins__'; import { useToken } from '../__builtins__';
import { designerCss } from './Table.Column.ActionBar'; import { designerCss } from './Table.Column.ActionBar';
@ -77,6 +79,7 @@ export const TableColumnDecorator = (props) => {
const compile = useCompile(); const compile = useCompile();
const { isInSubTable } = useFlag() || {}; const { isInSubTable } = useFlag() || {};
const { token } = useToken(); const { token } = useToken();
const { name } = useBlockContext?.() || {};
useEffect(() => { useEffect(() => {
if (field.title) { if (field.title) {
@ -110,11 +113,13 @@ export const TableColumnDecorator = (props) => {
})} })}
> >
<CollectionFieldContext.Provider value={collectionField}> <CollectionFieldContext.Provider value={collectionField}>
<BlockContext.Provider value={{ name: isInSubTable ? name : 'taleColumn' }}>
<Designer fieldSchema={fieldSchema} uiSchema={uiSchema} collectionField={collectionField} /> <Designer fieldSchema={fieldSchema} uiSchema={uiSchema} collectionField={collectionField} />
<span role="button"> <span role="button">
{fieldSchema?.required && <span className="ant-formily-item-asterisk">*</span>} {fieldSchema?.required && <span className="ant-formily-item-asterisk">*</span>}
<span>{field?.title || compile(uiSchema?.title)}</span> <span>{field?.title || compile(uiSchema?.title)}</span>
</span> </span>
</BlockContext.Provider>
</CollectionFieldContext.Provider> </CollectionFieldContext.Provider>
</SortableItem> </SortableItem>
); );

View File

@ -186,6 +186,7 @@ export type VariableInputProps = {
className?: string; className?: string;
parseOptions?: ParseOptions; parseOptions?: ParseOptions;
hideVariableButton?: boolean; hideVariableButton?: boolean;
constantAbel?: boolean;
}; };
export function Input(props: VariableInputProps) { export function Input(props: VariableInputProps) {
@ -202,6 +203,7 @@ export function Input(props: VariableInputProps) {
fieldNames, fieldNames,
parseOptions, parseOptions,
hideVariableButton, hideVariableButton,
constantAbel = true,
} = props; } = props;
const scope = typeof props.scope === 'function' ? props.scope() : props.scope; const scope = typeof props.scope === 'function' ? props.scope() : props.scope;
const { wrapSSR, hashId, componentCls, rootPrefixCls } = useStyles({ hideVariableButton }); const { wrapSSR, hashId, componentCls, rootPrefixCls } = useStyles({ hideVariableButton });
@ -233,6 +235,7 @@ export function Input(props: VariableInputProps) {
); );
const constantOption: DefaultOptionType & { component?: React.FC<any> } = useMemo(() => { const constantOption: DefaultOptionType & { component?: React.FC<any> } = useMemo(() => {
if (!constantAbel) return null;
if (children) { if (children) {
return { return {
value: '$', value: '$',

View File

@ -57,6 +57,7 @@ export const getTargetField = (obj) => {
} }
}); });
const result = keys.slice(0, index); const result = keys.slice(0, index);
return result; return result;
}; };
@ -76,43 +77,89 @@ function getAllKeys(obj) {
return keys; return keys;
} }
const parseVariableValue = async (targetVariable, variables, localVariables) => {
const parsingResult = isVariable(targetVariable)
? [variables.parseVariable(targetVariable, localVariables).then(({ value }) => value)]
: [targetVariable];
try {
const [value] = await Promise.all(parsingResult);
return value;
} catch (error) {
console.error('Error in parseVariableValue:', error);
throw error;
}
};
export const conditionAnalyses = async ( export const conditionAnalyses = async (
{ {
ruleGroup, ruleGroup,
variables, variables,
localVariables, localVariables,
variableNameOfLeftCondition, variableNameOfLeftCondition,
conditionType,
}: { }: {
ruleGroup; ruleGroup;
variables: VariablesContextType; variables: VariablesContextType;
localVariables: VariableOption[]; localVariables: VariableOption[];
/**
* used to parse the variable name of the left condition value
* @default '$nForm'
*/
variableNameOfLeftCondition?: string; variableNameOfLeftCondition?: string;
conditionType?: 'advanced' | 'basic';
}, },
jsonLogic: any, jsonLogic: any,
) => { ) => {
const type = Object.keys(ruleGroup)[0] || '$and'; const type = Object.keys(ruleGroup)[0] || '$and';
const conditions = ruleGroup[type]; const conditions = ruleGroup[type];
let results = conditions.map(async (condition) => {
if ('$and' in condition || '$or' in condition) { const results = await Promise.all(
return await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic); conditions.map((condition) =>
processCondition(condition, variables, localVariables, variableNameOfLeftCondition, conditionType, jsonLogic),
),
);
if (type === '$and') {
return every(results, (v) => v);
} else {
if (results.length) {
return some(results, (v) => v);
} }
const logicCalculation = getInnermostKeyAndValue(condition);
const operator = logicCalculation?.key;
if (!operator) {
return true; return true;
} }
};
const processCondition = async (
condition,
variables,
localVariables,
variableNameOfLeftCondition,
conditionType,
jsonLogic,
) => {
if ('$and' in condition || '$or' in condition) {
return await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic);
}
return conditionType === 'advanced'
? processAdvancedCondition(condition, variables, localVariables, jsonLogic)
: processBasicCondition(condition, variables, localVariables, variableNameOfLeftCondition, jsonLogic);
};
const processAdvancedCondition = async (condition, variables, localVariables, jsonLogic) => {
const operator = condition.op;
const rightValue = await parseVariableValue(condition.rightVar, variables, localVariables);
const leftValue = await parseVariableValue(condition.leftVar, variables, localVariables);
if (operator) {
return jsonLogic.apply({ [operator]: [leftValue, rightValue] });
}
return true;
};
const processBasicCondition = async (condition, variables, localVariables, variableNameOfLeftCondition, jsonLogic) => {
const logicCalculation = getInnermostKeyAndValue(condition);
const operator = logicCalculation?.key;
if (!operator) return true;
const targetVariableName = targetFieldToVariableString(getTargetField(condition), variableNameOfLeftCondition); const targetVariableName = targetFieldToVariableString(getTargetField(condition), variableNameOfLeftCondition);
const targetValue = variables const targetValue = variables
.parseVariable(targetVariableName, localVariables, { .parseVariable(targetVariableName, localVariables, { doNotRequest: true })
doNotRequest: true,
})
.then(({ value }) => value); .then(({ value }) => value);
const parsingResult = isVariable(logicCalculation?.value) const parsingResult = isVariable(logicCalculation?.value)
@ -120,10 +167,11 @@ export const conditionAnalyses = async (
: [logicCalculation?.value, targetValue]; : [logicCalculation?.value, targetValue];
try { try {
const [value, targetValue] = await Promise.all(parsingResult); const [value, resolvedTargetValue] = await Promise.all(parsingResult);
const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables); const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables);
let currentInputValue = transformVariableValue(targetValue, { targetCollectionField }); let currentInputValue = transformVariableValue(resolvedTargetValue, { targetCollectionField });
const comparisonValue = transformVariableValue(value, { targetCollectionField }); const comparisonValue = transformVariableValue(value, { targetCollectionField });
if ( if (
targetCollectionField?.type && targetCollectionField?.type &&
['datetime', 'date', 'datetimeNoTz', 'dateOnly', 'unixTimestamp'].includes(targetCollectionField.type) && ['datetime', 'date', 'datetimeNoTz', 'dateOnly', 'unixTimestamp'].includes(targetCollectionField.type) &&
@ -133,24 +181,10 @@ export const conditionAnalyses = async (
const format = getPickerFormat(picker); const format = getPickerFormat(picker);
currentInputValue = dayjs(currentInputValue).format(format); currentInputValue = dayjs(currentInputValue).format(format);
} }
return jsonLogic.apply({ [operator]: [currentInputValue, comparisonValue] });
return jsonLogic.apply({
[operator]: [currentInputValue, comparisonValue],
});
} catch (error) { } catch (error) {
throw error; throw error;
} }
});
results = await Promise.all(results);
if (type === '$and') {
return every(results, (v) => v);
} else {
if (results.length) {
return some(results, (v) => v);
}
return true;
}
}; };
/** /**

View File

@ -89,6 +89,7 @@ const InternalCreateRecordAction = (props: any, ref) => {
condition: v.condition, condition: v.condition,
variables, variables,
localVariables, localVariables,
conditionType: v.conditionType,
}, },
app.jsonLogic, app.jsonLogic,
); );
@ -208,6 +209,7 @@ export const CreateAction = observer(
condition: v.condition, condition: v.condition,
variables, variables,
localVariables, localVariables,
conditionType: v.conditionType,
}, },
app.jsonLogic, app.jsonLogic,
); );

View File

@ -26,7 +26,7 @@ import { VariableInput, getShouldChange } from '../../../schema-settings/Variabl
import { Option } from '../../../schema-settings/VariableInput/type'; import { Option } from '../../../schema-settings/VariableInput/type';
import { formatVariableScop } from '../../../schema-settings/VariableInput/utils/formatVariableScop'; import { formatVariableScop } from '../../../schema-settings/VariableInput/utils/formatVariableScop';
import { useLocalVariables, useVariables } from '../../../variables'; import { useLocalVariables, useVariables } from '../../../variables';
import { BlockContext, useBlockContext } from '../../../block-provider';
interface AssignedFieldProps { interface AssignedFieldProps {
value: any; value: any;
onChange: (value: any) => void; onChange: (value: any) => void;
@ -93,7 +93,7 @@ export enum AssignedFieldValueType {
DynamicValue = 'dynamicValue', DynamicValue = 'dynamicValue',
} }
export const AssignedField = (props: AssignedFieldProps) => { export const AssignedFieldInner = (props: AssignedFieldProps) => {
const { value, onChange } = props; const { value, onChange } = props;
const { getCollectionFields, getAllCollectionsInheritChain } = useCollectionManager_deprecated(); const { getCollectionFields, getAllCollectionsInheritChain } = useCollectionManager_deprecated();
const collection = useCollection_deprecated(); const collection = useCollection_deprecated();
@ -148,3 +148,13 @@ export const AssignedField = (props: AssignedFieldProps) => {
/> />
); );
}; };
export const AssignedField = (props) => {
const { form } = useFormBlockContext();
const { name } = useBlockContext();
return (
<BlockContext.Provider value={{ name: form ? 'form' : name }}>
<AssignedFieldInner {...props} />
</BlockContext.Provider>
);
};

View File

@ -728,7 +728,7 @@ export const useCustomFormItemInitializerFields = (options?: any) => {
const remove = useRemoveGridFormItem(); const remove = useRemoveGridFormItem();
return currentFields return currentFields
?.filter((field) => { ?.filter((field) => {
return field?.interface && field.interface !== 'snapshot' && field.type !== 'sequence'; return !field.inherit && field?.interface && field.interface !== 'snapshot' && field.type !== 'sequence';
}) })
?.map((field) => { ?.map((field) => {
const interfaceConfig = getInterface(field.interface); const interfaceConfig = getInterface(field.interface);

View File

@ -38,6 +38,7 @@ interface Props {
*/ */
variableNameOfLeftCondition?: string; variableNameOfLeftCondition?: string;
action?: any; action?: any;
conditionType?: 'advanced' | 'basic';
} }
export function bindLinkageRulesToFiled( export function bindLinkageRulesToFiled(
@ -83,7 +84,6 @@ export function bindLinkageRulesToFiled(
() => { () => {
// 获取条件中的字段值 // 获取条件中的字段值
const fieldValuesInCondition = getFieldValuesInCondition({ linkageRules, formValues }); const fieldValuesInCondition = getFieldValuesInCondition({ linkageRules, formValues });
// 获取条件中的变量值 // 获取条件中的变量值
const variableValuesInCondition = getVariableValuesInCondition({ linkageRules, localVariables }); const variableValuesInCondition = getVariableValuesInCondition({ linkageRules, localVariables });
@ -132,7 +132,23 @@ function getVariableValuesInCondition({
return linkageRules.map((rule) => { return linkageRules.map((rule) => {
const type = Object.keys(rule.condition)[0] || '$and'; const type = Object.keys(rule.condition)[0] || '$and';
const conditions = rule.condition[type]; const conditions = rule.condition[type];
if (rule.conditionType === 'advanced') {
return conditions
.map((condition) => {
if (!condition) {
return null;
}
const resolveVariable = (varName) =>
isVariable(varName) ? getVariableValue(varName, localVariables) : varName;
return {
leftVar: resolveVariable(condition.leftVar),
rightVar: resolveVariable(condition.rightVar),
};
})
.filter(Boolean);
} else {
return conditions return conditions
.map((condition) => { .map((condition) => {
const jsonlogic = getInnermostKeyAndValue(condition); const jsonlogic = getInnermostKeyAndValue(condition);
@ -146,6 +162,7 @@ function getVariableValuesInCondition({
return jsonlogic.value; return jsonlogic.value;
}) })
.filter(Boolean); .filter(Boolean);
}
}); });
} }
@ -216,6 +233,7 @@ function getSubscriber(
localVariables, localVariables,
variableNameOfLeftCondition, variableNameOfLeftCondition,
action, action,
conditionType: rule.conditionType,
}, },
jsonLogic, jsonLogic,
); );
@ -327,7 +345,17 @@ function getFieldNameByOperator(operator: ActionType) {
} }
export const collectFieldStateOfLinkageRules = ( export const collectFieldStateOfLinkageRules = (
{ operator, value, field, condition, variables, localVariables, variableNameOfLeftCondition, action }: Props, {
operator,
value,
field,
condition,
variables,
localVariables,
variableNameOfLeftCondition,
action,
conditionType,
}: Props,
jsonLogic: any, jsonLogic: any,
) => { ) => {
const requiredResult = field?.stateOfLinkageRules?.required || [field?.initStateOfLinkageRules?.required]; const requiredResult = field?.stateOfLinkageRules?.required || [field?.initStateOfLinkageRules?.required];
@ -336,7 +364,13 @@ export const collectFieldStateOfLinkageRules = (
const valueResult = field?.stateOfLinkageRules?.value || [field?.initStateOfLinkageRules?.value]; const valueResult = field?.stateOfLinkageRules?.value || [field?.initStateOfLinkageRules?.value];
const optionsResult = field?.stateOfLinkageRules?.dataSource || [field?.initStateOfLinkageRules?.dataSource]; const optionsResult = field?.stateOfLinkageRules?.dataSource || [field?.initStateOfLinkageRules?.dataSource];
const { evaluate } = evaluators.get('formula.js'); const { evaluate } = evaluators.get('formula.js');
const paramsToGetConditionResult = { ruleGroup: condition, variables, localVariables, variableNameOfLeftCondition }; const paramsToGetConditionResult = {
ruleGroup: condition,
variables,
localVariables,
variableNameOfLeftCondition,
conditionType,
};
const dateScopeResult = field?.stateOfLinkageRules?.dateScope || [field?.initStateOfLinkageRules?.dateScope]; const dateScopeResult = field?.stateOfLinkageRules?.dateScope || [field?.initStateOfLinkageRules?.dateScope];
switch (operator) { switch (operator) {

View File

@ -38,7 +38,12 @@ const getSatisfiedActions = async ({ rules, variables, localVariables }, jsonLog
rules rules
.filter((k) => !k.disabled) .filter((k) => !k.disabled)
.map(async (rule) => { .map(async (rule) => {
if (await conditionAnalyses({ ruleGroup: rule.condition, variables, localVariables }, jsonLogic)) { if (
await conditionAnalyses(
{ ruleGroup: rule.condition, variables, localVariables, conditionType: rule.conditionType },
jsonLogic,
)
) {
return rule; return rule;
} else return null; } else return null;
}), }),

View File

@ -10,7 +10,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { observer, useFieldSchema } from '@formily/react'; import { observer, useFieldSchema } from '@formily/react';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useCollectionManager_deprecated } from '../../collection-manager'; import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager';
import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider'; import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider';
import { CollectionProvider } from '../../data-source/collection/CollectionProvider'; import { CollectionProvider } from '../../data-source/collection/CollectionProvider';
import { withDynamicSchemaProps } from '../../hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../../hoc/withDynamicSchemaProps';
@ -27,15 +27,75 @@ import { ArrayCollapse } from './components/LinkageHeader';
export interface Props { export interface Props {
dynamicComponent: any; dynamicComponent: any;
} }
function extractFieldPath(obj, path = []) {
if (typeof obj !== 'object' || obj === null) return null;
const [key, value] = Object.entries(obj)[0] || [];
if (typeof value === 'object' && value !== null && !key.startsWith('$')) {
return extractFieldPath(value, [...path, key]);
}
return [path.join('.'), obj];
}
type Condition = { [field: string]: { [op: string]: any } } | { $and: Condition[] } | { $or: Condition[] };
type TransformedCondition =
| { leftVar: string; op: string; rightVar: any }
| { $and: TransformedCondition[] }
| { $or: TransformedCondition[] };
function transformConditionData(condition: Condition, variableKey: '$nForm' | '$nRecord'): TransformedCondition {
if ('$and' in condition) {
return {
$and: condition.$and.map((c) => transformConditionData(c, variableKey)),
};
}
if ('$or' in condition) {
return {
$or: condition.$or.map((c) => transformConditionData(c, variableKey)),
};
}
const [field, expression] = extractFieldPath(condition || {}) || [];
const [op, value] = Object.entries(expression || {})[0] || [];
return {
leftVar: field ? `{{${variableKey}.${field}}}` : null,
op,
rightVar: value,
};
}
function getActiveContextName(contextList: { name: string; ctx: any }[]): string | null {
const priority = ['$nForm', '$nRecord'];
for (const name of priority) {
const item = contextList.find((ctx) => ctx.name === name && ctx.ctx);
if (item) return name;
}
return '$nRecord';
}
const transformDefaultValue = (values, variableKey) => {
return values.map((v) => {
if (v.conditionType !== 'advanced') {
const condition = transformConditionData(v.condition, variableKey);
return {
...v,
condition: variableKey ? condition : v.condition,
conditionType: variableKey ? 'advanced' : 'basic',
};
}
return v;
});
};
export const FormLinkageRules = withDynamicSchemaProps( export const FormLinkageRules = withDynamicSchemaProps(
observer((props: Props) => { observer((props: Props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { options, defaultValues, collectionName, form, variables, localVariables, record, dynamicComponent } = const { options, defaultValues, collectionName, form, variables, localVariables, record, dynamicComponent } =
useProps(props); // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema useProps(props); // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { name } = useCollection_deprecated();
const { getAllCollectionsInheritChain } = useCollectionManager_deprecated(); const { getAllCollectionsInheritChain } = useCollectionManager_deprecated();
const parentRecordData = useCollectionParentRecordData(); const parentRecordData = useCollectionParentRecordData();
const variableKey = getActiveContextName(localVariables);
const components = useMemo(() => ({ ArrayCollapse }), []); const components = useMemo(() => ({ ArrayCollapse }), []);
const schema = useMemo( const schema = useMemo(
() => ({ () => ({
@ -43,7 +103,7 @@ export const FormLinkageRules = withDynamicSchemaProps(
properties: { properties: {
rules: { rules: {
type: 'array', type: 'array',
default: defaultValues, default: transformDefaultValue(defaultValues, variableKey),
'x-component': 'ArrayCollapse', 'x-component': 'ArrayCollapse',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component-props': { 'x-component-props': {
@ -72,6 +132,20 @@ export const FormLinkageRules = withDynamicSchemaProps(
'x-content': '{{ t("Condition") }}', 'x-content': '{{ t("Condition") }}',
}, },
condition: { condition: {
'x-component': 'Input', // 仅作为数据存储
'x-hidden': true, // 不显示
'x-reactions': [
{
dependencies: ['.conditionType', '.conditionBasic', '.conditionAdvanced'],
fulfill: {
state: {
value: '{{$deps[0] === "basic" ? $deps[1] : $deps[2]}}',
},
},
},
],
},
conditionBasic: {
'x-component': 'Filter', 'x-component': 'Filter',
'x-use-component-props': () => { 'x-use-component-props': () => {
return { return {
@ -83,6 +157,7 @@ export const FormLinkageRules = withDynamicSchemaProps(
`, `,
}; };
}, },
'x-visible': '{{$deps[0] === "basic"}}',
'x-component-props': { 'x-component-props': {
collectionName, collectionName,
dynamicComponent: (props: DynamicComponentProps) => { dynamicComponent: (props: DynamicComponentProps) => {
@ -102,6 +177,38 @@ export const FormLinkageRules = withDynamicSchemaProps(
); );
}, },
}, },
'x-reactions': [
{
dependencies: ['.conditionType', '.condition'],
fulfill: {
state: {
visible: '{{$deps[0] === "basic"}}',
value: '{{$deps[0] === "basic" ? $deps[1] : undefined}}',
},
},
},
],
},
conditionAdvanced: {
'x-component': 'LinkageFilter',
'x-visible': '{{$deps[0] === "advanced"}}',
'x-reactions': [
{
dependencies: ['.conditionType', '.condition'],
fulfill: {
state: {
visible: '{{$deps[0] === "advanced"}}',
value: '{{$deps[0] === "advanced" ? $deps[1] : undefined}}',
},
},
},
],
},
conditionType: {
type: 'string',
'x-component': 'Input',
default: 'advanced',
'x-hidden': true,
}, },
actions: { actions: {
'x-component': 'h4', 'x-component': 'h4',
@ -168,10 +275,10 @@ export const FormLinkageRules = withDynamicSchemaProps(
return ( return (
// 这里使用 SubFormProvider 包裹,是为了让子表格的联动规则中 “当前对象” 的配置显示正确 // 这里使用 SubFormProvider 包裹,是为了让子表格的联动规则中 “当前对象” 的配置显示正确
<SubFormProvider value={{ value: null, collection: { name: collectionName } as any }}> <SubFormProvider value={{ value: null, collection: { name: collectionName || name } as any }}>
<RecordProvider record={record} parent={parentRecordData}> <RecordProvider record={record} parent={parentRecordData}>
<FilterContext.Provider value={value}> <FilterContext.Provider value={value}>
<CollectionProvider name={collectionName}> <CollectionProvider name={collectionName || name} allowNull>
<SchemaComponent components={components} schema={schema} /> <SchemaComponent components={components} schema={schema} />
</CollectionProvider> </CollectionProvider>
</FilterContext.Provider> </FilterContext.Provider>

View File

@ -32,9 +32,11 @@ export enum ActionType {
export enum LinkageRuleCategory { export enum LinkageRuleCategory {
default = 'default', default = 'default',
style = 'style', style = 'style',
button = 'button',
} }
export const LinkageRuleDataKeyMap: Record<`${LinkageRuleCategory}`, string> = { export const LinkageRuleDataKeyMap: Record<`${LinkageRuleCategory}`, string> = {
[LinkageRuleCategory.style]: 'x-linkage-style-rules', [LinkageRuleCategory.style]: 'x-linkage-style-rules',
[LinkageRuleCategory.default]: 'x-linkage-rules', [LinkageRuleCategory.default]: 'x-linkage-rules',
[LinkageRuleCategory.button]: 'x-linkage-rules',
}; };

View File

@ -1122,7 +1122,7 @@ export const SchemaSettingsLinkageRules = function LinkageRules(props) {
const getRules = useCallback(() => { const getRules = useCallback(() => {
return gridSchema?.[dataKey] || fieldSchema?.[dataKey] || []; return gridSchema?.[dataKey] || fieldSchema?.[dataKey] || [];
}, [gridSchema, fieldSchema, dataKey]); }, [gridSchema, fieldSchema, dataKey]);
const title = titleMap[category]; const title = titleMap[category] || t('Linkage rules');
const schema = useMemo<ISchema>( const schema = useMemo<ISchema>(
() => ({ () => ({
type: 'object', type: 'object',
@ -1155,7 +1155,7 @@ export const SchemaSettingsLinkageRules = function LinkageRules(props) {
(v) => { (v) => {
const rules = []; const rules = [];
for (const rule of v.fieldReaction.rules) { for (const rule of v.fieldReaction.rules) {
rules.push(_.pickBy(rule, _.identity)); rules.push(_.omit(_.pickBy(rule, _.identity), ['conditionBasic', 'conditionAdvanced']));
} }
const templateId = gridSchema['x-component'] === 'BlockTemplate' && gridSchema['x-component-props']?.templateId; const templateId = gridSchema['x-component'] === 'BlockTemplate' && gridSchema['x-component-props']?.templateId;
const uid = (templateId && getTemplateById(templateId).uid) || gridSchema['x-uid']; const uid = (templateId && getTemplateById(templateId).uid) || gridSchema['x-uid'];

View File

@ -11,7 +11,7 @@ import { Form } from '@formily/core';
// @ts-ignore // @ts-ignore
import { Schema } from '@formily/json-schema'; import { Schema } from '@formily/json-schema';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CollectionFieldOptions_deprecated } from '../../collection-manager'; import { CollectionFieldOptions_deprecated } from '../../collection-manager';
import { Variable, useVariableScope } from '../../schema-component'; import { Variable, useVariableScope } from '../../schema-component';
@ -72,6 +72,10 @@ type Props = {
*/ */
noDisabled?: boolean; noDisabled?: boolean;
hideVariableButton?: boolean; hideVariableButton?: boolean;
setScopes?: any; //更新scopes
nullable?: boolean;
constantAbel?: boolean;
changeOnSelect?: boolean;
}; };
/** /**
@ -98,6 +102,10 @@ export const VariableInput = (props: Props) => {
targetFieldSchema, targetFieldSchema,
noDisabled, noDisabled,
hideVariableButton, hideVariableButton,
setScopes,
nullable = true,
constantAbel = true,
changeOnSelect = true,
} = props; } = props;
const { name: blockCollectionName } = useBlockCollection(); const { name: blockCollectionName } = useBlockCollection();
const scope = useVariableScope(); const scope = useVariableScope();
@ -127,31 +135,37 @@ export const VariableInput = (props: Props) => {
const handleChange = useCallback( const handleChange = useCallback(
(value: any, optionPath: any[]) => { (value: any, optionPath: any[]) => {
if (!shouldChange) { if (!shouldChange) {
return onChange(value); return onChange(value, optionPath);
} }
// `shouldChange` 这个函数的运算量比较大,会导致展开变量列表时有明显的卡顿感,在这里加个延迟能有效解决这个问题 // `shouldChange` 这个函数的运算量比较大,会导致展开变量列表时有明显的卡顿感,在这里加个延迟能有效解决这个问题
setTimeout(async () => { setTimeout(async () => {
if (await shouldChange(value, optionPath)) { if (await shouldChange(value, optionPath)) {
onChange(value); onChange(value, optionPath);
} }
}); });
}, },
[onChange, shouldChange], [onChange, shouldChange],
); );
const scopes = returnScope(
compatOldVariables(_.isEmpty(scope) ? variableOptions : scope, {
value,
}),
);
useEffect(() => {
setScopes?.(scopes);
}, [value, scope]);
return ( return (
<Variable.Input <Variable.Input
className={className} className={className}
value={value} value={value}
onChange={handleChange} onChange={handleChange}
scope={returnScope( scope={scopes}
compatOldVariables(_.isEmpty(scope) ? variableOptions : scope, {
value,
}),
)}
style={style} style={style}
changeOnSelect changeOnSelect={changeOnSelect}
hideVariableButton={hideVariableButton} hideVariableButton={hideVariableButton}
nullable={nullable}
constantAbel={constantAbel}
> >
<RenderSchemaComponent value={value} onChange={onChange} /> <RenderSchemaComponent value={value} onChange={onChange} />
</Variable.Input> </Variable.Input>

View File

@ -9,6 +9,7 @@
import { useAPIClient } from '../../../api-client/hooks/useAPIClient'; import { useAPIClient } from '../../../api-client/hooks/useAPIClient';
import { useBaseVariable } from './useBaseVariable'; import { useBaseVariable } from './useBaseVariable';
import { string } from '../../../collection-manager/interfaces/properties/operators';
/** /**
* `当前 Token` * `当前 Token`
@ -26,6 +27,7 @@ export const useAPITokenVariable = ({
title: 'API token', title: 'API token',
noDisabled, noDisabled,
noChildren: true, noChildren: true,
operators: string,
}); });
return { return {

View File

@ -87,6 +87,8 @@ interface BaseProps {
*/ */
deprecated?: boolean; deprecated?: boolean;
tooltip?: string; tooltip?: string;
/**支持的操作符 */
operators?: any[];
} }
interface BaseVariableProviderProps { interface BaseVariableProviderProps {
@ -133,6 +135,8 @@ const getChildren = (
: isDisabled({ option, collectionField, uiSchema, targetFieldSchema, getCollectionField })), : isDisabled({ option, collectionField, uiSchema, targetFieldSchema, getCollectionField })),
isLeaf: true, isLeaf: true,
depth, depth,
operators: option?.operators,
schema: option?.schema,
}; };
} }
@ -197,6 +201,7 @@ export const useBaseVariable = ({
returnFields = (fields) => fields, returnFields = (fields) => fields,
deprecated, deprecated,
tooltip, tooltip,
operators = [],
}: BaseProps) => { }: BaseProps) => {
const compile = useCompile(); const compile = useCompile();
const getFilterOptions = useGetFilterOptions(); const getFilterOptions = useGetFilterOptions();
@ -276,6 +281,7 @@ export const useBaseVariable = ({
children: [], children: [],
disabled: !!deprecated, disabled: !!deprecated,
deprecated, deprecated,
operators,
} as Option; } as Option;
}, [uiSchema?.['x-component']]); }, [uiSchema?.['x-component']]);

View File

@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
import { useOperators } from '../../../block-provider/CollectOperators'; import { useOperators } from '../../../block-provider/CollectOperators';
import { useDatePickerContext } from '../../../schema-component/antd/date-picker/DatePicker'; import { useDatePickerContext } from '../../../schema-component/antd/date-picker/DatePicker';
import { getDateRanges } from '../../../schema-component/antd/date-picker/util'; import { getDateRanges } from '../../../schema-component/antd/date-picker/util';
import { datetime } from '../../../collection-manager/interfaces/properties/operators';
interface Props { interface Props {
operator?: { operator?: {
value: string; value: string;
@ -45,132 +45,155 @@ export const useDateVariable = ({ operator, schema, noDisabled }: Props) => {
value: 'now', value: 'now',
label: t('Current time'), label: t('Current time'),
disabled: noDisabled ? false : schema?.['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween', disabled: noDisabled ? false : schema?.['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween',
operators: datetime,
schema: {},
}, },
{ {
key: 'yesterday', key: 'yesterday',
value: 'yesterday', value: 'yesterday',
label: t('Yesterday'), label: t('Yesterday'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'today', key: 'today',
value: 'today', value: 'today',
label: t('Today'), label: t('Today'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'tomorrow', key: 'tomorrow',
value: 'tomorrow', value: 'tomorrow',
label: t('Tomorrow'), label: t('Tomorrow'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'lastIsoWeek', key: 'lastIsoWeek',
value: 'lastIsoWeek', value: 'lastIsoWeek',
label: t('Last week'), label: t('Last week'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'thisIsoWeek', key: 'thisIsoWeek',
value: 'thisIsoWeek', value: 'thisIsoWeek',
label: t('This week'), label: t('This week'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'nextIsoWeek', key: 'nextIsoWeek',
value: 'nextIsoWeek', value: 'nextIsoWeek',
label: t('Next week'), label: t('Next week'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'lastMonth', key: 'lastMonth',
value: 'lastMonth', value: 'lastMonth',
label: t('Last month'), label: t('Last month'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'thisMonth', key: 'thisMonth',
value: 'thisMonth', value: 'thisMonth',
label: t('This month'), label: t('This month'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'nextMonth', key: 'nextMonth',
value: 'nextMonth', value: 'nextMonth',
label: t('Next month'), label: t('Next month'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'lastQuarter', key: 'lastQuarter',
value: 'lastQuarter', value: 'lastQuarter',
label: t('Last quarter'), label: t('Last quarter'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'thisQuarter', key: 'thisQuarter',
value: 'thisQuarter', value: 'thisQuarter',
label: t('This quarter'), label: t('This quarter'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'nextQuarter', key: 'nextQuarter',
value: 'nextQuarter', value: 'nextQuarter',
label: t('Next quarter'), label: t('Next quarter'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'lastYear', key: 'lastYear',
value: 'lastYear', value: 'lastYear',
label: t('Last year'), label: t('Last year'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'thisYear', key: 'thisYear',
value: 'thisYear', value: 'thisYear',
label: t('This year'), label: t('This year'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'nextYear', key: 'nextYear',
value: 'nextYear', value: 'nextYear',
label: t('Next year'), label: t('Next year'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'last7Days', key: 'last7Days',
value: 'last7Days', value: 'last7Days',
label: t('Last 7 days'), label: t('Last 7 days'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'next7Days', key: 'next7Days',
value: 'next7Days', value: 'next7Days',
label: t('Next 7 days'), label: t('Next 7 days'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'last30Days', key: 'last30Days',
value: 'last30Days', value: 'last30Days',
label: t('Last 30 days'), label: t('Last 30 days'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'next30Days', key: 'next30Days',
value: 'next30Days', value: 'next30Days',
label: t('Next 30 days'), label: t('Next 30 days'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'last90Days', key: 'last90Days',
value: 'last90Days', value: 'last90Days',
label: t('Last 90 days'), label: t('Last 90 days'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'next90Days', key: 'next90Days',
value: 'next90Days', value: 'next90Days',
label: t('Next 90 days'), label: t('Next 90 days'),
disabled, disabled,
operators: datetime,
}, },
]; ];
@ -222,132 +245,154 @@ export const useDatetimeVariable = ({ operator, schema, noDisabled, targetFieldS
value: 'now', value: 'now',
label: t('Current time'), label: t('Current time'),
disabled: noDisabled ? false : schema?.['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween', disabled: noDisabled ? false : schema?.['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween',
operators: datetime,
}, },
{ {
key: 'yesterday', key: 'yesterday',
value: 'yesterday', value: 'yesterday',
label: t('Yesterday'), label: t('Yesterday'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'today', key: 'today',
value: 'today', value: 'today',
label: t('Today'), label: t('Today'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'tomorrow', key: 'tomorrow',
value: 'tomorrow', value: 'tomorrow',
label: t('Tomorrow'), label: t('Tomorrow'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'lastIsoWeek', key: 'lastIsoWeek',
value: 'lastIsoWeek', value: 'lastIsoWeek',
label: t('Last week'), label: t('Last week'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'thisIsoWeek', key: 'thisIsoWeek',
value: 'thisIsoWeek', value: 'thisIsoWeek',
label: t('This week'), label: t('This week'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'nextIsoWeek', key: 'nextIsoWeek',
value: 'nextIsoWeek', value: 'nextIsoWeek',
label: t('Next week'), label: t('Next week'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'lastMonth', key: 'lastMonth',
value: 'lastMonth', value: 'lastMonth',
label: t('Last month'), label: t('Last month'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'thisMonth', key: 'thisMonth',
value: 'thisMonth', value: 'thisMonth',
label: t('This month'), label: t('This month'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'nextMonth', key: 'nextMonth',
value: 'nextMonth', value: 'nextMonth',
label: t('Next month'), label: t('Next month'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'lastQuarter', key: 'lastQuarter',
value: 'lastQuarter', value: 'lastQuarter',
label: t('Last quarter'), label: t('Last quarter'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'thisQuarter', key: 'thisQuarter',
value: 'thisQuarter', value: 'thisQuarter',
label: t('This quarter'), label: t('This quarter'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'nextQuarter', key: 'nextQuarter',
value: 'nextQuarter', value: 'nextQuarter',
label: t('Next quarter'), label: t('Next quarter'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'lastYear', key: 'lastYear',
value: 'lastYear', value: 'lastYear',
label: t('Last year'), label: t('Last year'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'thisYear', key: 'thisYear',
value: 'thisYear', value: 'thisYear',
label: t('This year'), label: t('This year'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'nextYear', key: 'nextYear',
value: 'nextYear', value: 'nextYear',
label: t('Next year'), label: t('Next year'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'last7Days', key: 'last7Days',
value: 'last7Days', value: 'last7Days',
label: t('Last 7 days'), label: t('Last 7 days'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'next7Days', key: 'next7Days',
value: 'next7Days', value: 'next7Days',
label: t('Next 7 days'), label: t('Next 7 days'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'last30Days', key: 'last30Days',
value: 'last30Days', value: 'last30Days',
label: t('Last 30 days'), label: t('Last 30 days'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'next30Days', key: 'next30Days',
value: 'next30Days', value: 'next30Days',
label: t('Next 30 days'), label: t('Next 30 days'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'last90Days', key: 'last90Days',
value: 'last90Days', value: 'last90Days',
label: t('Last 90 days'), label: t('Last 90 days'),
disabled, disabled,
operators: datetime,
}, },
{ {
key: 'next90Days', key: 'next90Days',
value: 'next90Days', value: 'next90Days',
label: t('Next 90 days'), label: t('Next 90 days'),
disabled, disabled,
operators: datetime,
}, },
]; ];

View File

@ -10,6 +10,7 @@
import { Form } from '@formily/core'; import { Form } from '@formily/core';
import { Schema } from '@formily/json-schema'; import { Schema } from '@formily/json-schema';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useBlockContext } from '../../../block-provider';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider'; import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import { CollectionFieldOptions_deprecated } from '../../../collection-manager'; import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
import { useDataBlockRequestData, useDataSource } from '../../../data-source'; import { useDataBlockRequestData, useDataSource } from '../../../data-source';
@ -62,14 +63,6 @@ export const useFormVariable = ({ collectionName, collectionField, schema, noDis
return result; return result;
}; };
const useCurrentFormData = () => {
const data = useDataBlockRequestData();
if (data?.data?.length > 1) {
return;
}
return data?.data?.[0] || data?.data;
};
/** /**
* `当前表单` hook * `当前表单` hook
* @param param0 * @param param0
@ -78,14 +71,14 @@ const useCurrentFormData = () => {
export const useCurrentFormContext = ({ form: _form }: Pick<Props, 'form'> = {}) => { export const useCurrentFormContext = ({ form: _form }: Pick<Props, 'form'> = {}) => {
const { form } = useFormBlockContext(); const { form } = useFormBlockContext();
const { isVariableParsedInOtherContext } = useFlag(); const { isVariableParsedInOtherContext } = useFlag();
const { name } = useBlockContext?.() || {};
const formInstance = _form || form; const formInstance = _form || form;
return { return {
/** 变量值 */ /** 变量值 */
currentFormCtx: formInstance?.values, currentFormCtx: formInstance?.values,
/** 用来判断是否可以显示`当前表单`变量 */ /** 用来判断是否可以显示`当前表单`变量 */
shouldDisplayCurrentForm: formInstance && !formInstance.readPretty && !isVariableParsedInOtherContext, shouldDisplayCurrentForm:
name === 'form' && formInstance && !formInstance.readPretty && !isVariableParsedInOtherContext,
}; };
}; };

View File

@ -90,7 +90,10 @@ export const useCurrentRecordContext = () => {
/** 变量值 */ /** 变量值 */
currentRecordCtx: ctx?.recordData || formRecord?.data || recordData, currentRecordCtx: ctx?.recordData || formRecord?.data || recordData,
/** 用于判断是否需要显示配置项 */ /** 用于判断是否需要显示配置项 */
shouldDisplayCurrentRecord: !_.isEmpty(_.omit(recordData, ['__collectionName', '__parent'])) || !!formRecord?.data, shouldDisplayCurrentRecord:
!_.isEmpty(_.omit(recordData, ['__collectionName', '__parent'])) ||
!!formRecord?.data ||
blockType === 'taleColumn',
/** 当前记录对应的 collection name */ /** 当前记录对应的 collection name */
collectionName: realCollectionName, collectionName: realCollectionName,
/** 块类型 */ /** 块类型 */

View File

@ -13,6 +13,9 @@ import { useAPIClient } from '../../../api-client';
import { CollectionFieldOptions_deprecated } from '../../../collection-manager'; import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
import { CollectionFieldOptions } from '../../../data-source/collection/Collection'; import { CollectionFieldOptions } from '../../../data-source/collection/Collection';
import { useBaseVariable } from './useBaseVariable'; import { useBaseVariable } from './useBaseVariable';
import { string } from '../../../collection-manager/interfaces/properties/operators';
import { useCurrentUserContext } from '../../../user/CurrentUserProvider';
import { useCompile } from '../../../schema-component';
/** /**
* @deprecated * @deprecated
@ -47,6 +50,7 @@ export const useRoleVariable = ({
noDisabled, noDisabled,
targetFieldSchema, targetFieldSchema,
noChildren: true, noChildren: true,
operators: string,
}); });
return result; return result;
@ -73,6 +77,9 @@ export const useCurrentRoleVariable = ({
} = {}) => { } = {}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const apiClient = useAPIClient(); const apiClient = useAPIClient();
const compile = useCompile();
const { data } = useCurrentUserContext() || {};
const roles = (data?.data?.roles || []).map(({ name, title }) => ({ name, title: compile(title) }));
const currentRoleSettings = useBaseVariable({ const currentRoleSettings = useBaseVariable({
collectionField, collectionField,
uiSchema, uiSchema,
@ -83,12 +90,13 @@ export const useCurrentRoleVariable = ({
noDisabled, noDisabled,
targetFieldSchema, targetFieldSchema,
noChildren: true, noChildren: true,
operators: string,
}); });
return { return {
/** 变量配置项 */ /** 变量配置项 */
currentRoleSettings, currentRoleSettings,
/** 变量的值 */ /** 变量的值 */
currentRoleCtx: apiClient.auth?.role, currentRoleCtx: apiClient.auth?.role === '__union__' ? roles.map((v) => v.name) : apiClient.auth?.role,
}; };
}; };

View File

@ -27,7 +27,7 @@ export const useIsLoggedIn = () => {
export const useCurrentRoles = () => { export const useCurrentRoles = () => {
const { allowAnonymous } = useACLRoleContext(); const { allowAnonymous } = useACLRoleContext();
const { data } = useCurrentUserContext(); const { data } = useCurrentUserContext() || {};
const compile = useCompile(); const compile = useCompile();
const options = useMemo(() => { const options = useMemo(() => {
const roles = (data?.data?.roles || []).map(({ name, title }) => ({ name, title: compile(title) })); const roles = (data?.data?.roles || []).map(({ name, title }) => ({ name, title: compile(title) }));

View File

@ -1,6 +1,6 @@
{ {
"name": "create-nocobase-app", "name": "create-nocobase-app",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"main": "src/index.js", "main": "src/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -8,6 +8,7 @@
"axios": "^1.7.0", "axios": "^1.7.0",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"commander": "^9.2.0", "commander": "^9.2.0",
"fs-extra": "^11.3.0",
"tar": "6.1.11" "tar": "6.1.11"
}, },
"bin": { "bin": {

View File

@ -19,6 +19,7 @@ const cli = new Command('create-nocobase');
cli cli
.arguments('<name>', 'directory of new NocoBase app') .arguments('<name>', 'directory of new NocoBase app')
.option('--quickstart', 'quickstart app creation') .option('--quickstart', 'quickstart app creation')
.option('--skip-dev-dependencies')
.option('-a, --all-db-dialect', 'install all database dialect dependencies') .option('-a, --all-db-dialect', 'install all database dialect dependencies')
.option('-d, --db-dialect <dbDialect>', 'database dialect, current support sqlite/mysql/postgres', 'sqlite') .option('-d, --db-dialect <dbDialect>', 'database dialect, current support sqlite/mysql/postgres', 'sqlite')
.option('-e, --env <env>', 'environment variables write into .env file', concat, []) .option('-e, --env <env>', 'environment variables write into .env file', concat, [])

View File

@ -9,7 +9,8 @@
const chalk = require('chalk'); const chalk = require('chalk');
const crypto = require('crypto'); const crypto = require('crypto');
const { existsSync } = require('fs'); const { existsSync, promises } = require('fs');
const fs = require('fs-extra');
const { join, resolve } = require('path'); const { join, resolve } = require('path');
const { Generator } = require('@umijs/utils'); const { Generator } = require('@umijs/utils');
const { downloadPackageFromNpm, updateJsonFile } = require('./util'); const { downloadPackageFromNpm, updateJsonFile } = require('./util');
@ -191,6 +192,13 @@ class AppGenerator extends Generator {
this.checkDbEnv(); this.checkDbEnv();
const skipDevDependencies = this.args.skipDevDependencies;
if (skipDevDependencies) {
const json = await fs.readJSON(join(this.cwd, 'package.json'), 'utf8');
delete json['devDependencies'];
await fs.writeJSON(join(this.cwd, 'package.json'), json, { encoding: 'utf8', spaces: 2 });
}
console.log(''); console.log('');
console.log(chalk.green(`$ cd ${name}`)); console.log(chalk.green(`$ cd ${name}`));
console.log(chalk.green(`$ yarn install`)); console.log(chalk.green(`$ yarn install`));

View File

@ -29,6 +29,7 @@
"react-router-dom": "6.28.1", "react-router-dom": "6.28.1",
"react-router": "6.28.1", "react-router": "6.28.1",
"antd": "5.24.2", "antd": "5.24.2",
"async": "3.2.6",
"rollup": "4.24.0" "rollup": "4.24.0"
}, },
"dependencies": { "dependencies": {

View File

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

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/database", "name": "@nocobase/database",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"description": "", "description": "",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@nocobase/logger": "1.7.0-beta.16", "@nocobase/logger": "1.7.0-beta.18",
"@nocobase/utils": "1.7.0-beta.16", "@nocobase/utils": "1.7.0-beta.18",
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"cron-parser": "4.4.0", "cron-parser": "4.4.0",
@ -20,12 +20,12 @@
"graphlib": "^2.1.8", "graphlib": "^2.1.8",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mathjs": "^10.6.1", "mathjs": "^10.6.1",
"nanoid": "^3.3.6", "nanoid": "^3.3.11",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"node-sql-parser": "^4.18.0", "node-sql-parser": "^4.18.0",
"qs": "^6.11.2", "qs": "^6.11.2",
"safe-json-stringify": "^1.2.0", "safe-json-stringify": "^1.2.0",
"semver": "^7.3.7", "semver": "^7.7.1",
"sequelize": "^6.26.0", "sequelize": "^6.26.0",
"umzug": "^3.1.1", "umzug": "^3.1.1",
"uuid": "^9.0.1" "uuid": "^9.0.1"

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/devtools", "name": "@nocobase/devtools",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./src/index.js", "main": "./src/index.js",
"dependencies": { "dependencies": {
"@nocobase/build": "1.7.0-beta.16", "@nocobase/build": "1.7.0-beta.18",
"@nocobase/client": "1.7.0-beta.16", "@nocobase/client": "1.7.0-beta.18",
"@nocobase/test": "1.7.0-beta.16", "@nocobase/test": "1.7.0-beta.18",
"@types/koa": "^2.15.0", "@types/koa": "^2.15.0",
"@types/koa-bodyparser": "^4.3.4", "@types/koa-bodyparser": "^4.3.4",
"@types/lodash": "^4.14.177", "@types/lodash": "^4.14.177",
@ -35,7 +35,7 @@
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"rimraf": "^3.0.0", "rimraf": "^3.0.0",
"serve": "^13.0.2", "serve": "^14.2.4",
"ts-loader": "^7.0.4", "ts-loader": "^7.0.4",
"ts-node": "9.1.1", "ts-node": "9.1.1",
"ts-node-dev": "1.1.8", "ts-node-dev": "1.1.8",

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/evaluators", "name": "@nocobase/evaluators",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"description": "", "description": "",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@formulajs/formulajs": "4.4.9", "@formulajs/formulajs": "4.4.9",
"@nocobase/utils": "1.7.0-beta.16", "@nocobase/utils": "1.7.0-beta.18",
"mathjs": "^10.6.0" "mathjs": "^10.6.0"
}, },
"repository": { "repository": {

View File

@ -1,10 +1,10 @@
{ {
"name": "@nocobase/lock-manager", "name": "@nocobase/lock-manager",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"main": "lib/index.js", "main": "lib/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {
"@nocobase/utils": "1.7.0-beta.16", "@nocobase/utils": "1.7.0-beta.18",
"async-mutex": "^0.5.0" "async-mutex": "^0.5.0"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/logger", "name": "@nocobase/logger",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"description": "nocobase logging library", "description": "nocobase logging library",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",

View File

@ -1,12 +1,12 @@
{ {
"name": "@nocobase/resourcer", "name": "@nocobase/resourcer",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"description": "", "description": "",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@nocobase/utils": "1.7.0-beta.16", "@nocobase/utils": "1.7.0-beta.18",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"koa-compose": "^4.1.0", "koa-compose": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/sdk", "name": "@nocobase/sdk",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/server", "name": "@nocobase/server",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"main": "lib/index.js", "main": "lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "AGPL-3.0", "license": "AGPL-3.0",
@ -10,19 +10,19 @@
"@koa/cors": "^5.0.0", "@koa/cors": "^5.0.0",
"@koa/multer": "^3.0.2", "@koa/multer": "^3.0.2",
"@koa/router": "^9.4.0", "@koa/router": "^9.4.0",
"@nocobase/acl": "1.7.0-beta.16", "@nocobase/acl": "1.7.0-beta.18",
"@nocobase/actions": "1.7.0-beta.16", "@nocobase/actions": "1.7.0-beta.18",
"@nocobase/auth": "1.7.0-beta.16", "@nocobase/auth": "1.7.0-beta.18",
"@nocobase/cache": "1.7.0-beta.16", "@nocobase/cache": "1.7.0-beta.18",
"@nocobase/data-source-manager": "1.7.0-beta.16", "@nocobase/data-source-manager": "1.7.0-beta.18",
"@nocobase/database": "1.7.0-beta.16", "@nocobase/database": "1.7.0-beta.18",
"@nocobase/evaluators": "1.7.0-beta.16", "@nocobase/evaluators": "1.7.0-beta.18",
"@nocobase/lock-manager": "1.7.0-beta.16", "@nocobase/lock-manager": "1.7.0-beta.18",
"@nocobase/logger": "1.7.0-beta.16", "@nocobase/logger": "1.7.0-beta.18",
"@nocobase/resourcer": "1.7.0-beta.16", "@nocobase/resourcer": "1.7.0-beta.18",
"@nocobase/sdk": "1.7.0-beta.16", "@nocobase/sdk": "1.7.0-beta.18",
"@nocobase/telemetry": "1.7.0-beta.16", "@nocobase/telemetry": "1.7.0-beta.18",
"@nocobase/utils": "1.7.0-beta.16", "@nocobase/utils": "1.7.0-beta.18",
"@types/decompress": "4.2.7", "@types/decompress": "4.2.7",
"@types/ini": "^1.3.31", "@types/ini": "^1.3.31",
"@types/koa-send": "^4.1.3", "@types/koa-send": "^4.1.3",
@ -31,6 +31,7 @@
"axios": "^1.7.0", "axios": "^1.7.0",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"commander": "^9.2.0", "commander": "^9.2.0",
"compression": "^1.8.0",
"cron": "^2.4.4", "cron": "^2.4.4",
"cronstrue": "^2.11.0", "cronstrue": "^2.11.0",
"dayjs": "^1.11.8", "dayjs": "^1.11.8",
@ -45,9 +46,9 @@
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "^1.4.2", "multer": "^1.4.2",
"nanoid": "3.3.4", "nanoid": "^3.3.11",
"semver": "^7.3.7", "semver": "^7.7.1",
"serve-handler": "^6.1.5", "serve-handler": "^6.1.6",
"ws": "^8.13.0", "ws": "^8.13.0",
"xpipe": "^1.0.5" "xpipe": "^1.0.5"
}, },

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/telemetry", "name": "@nocobase/telemetry",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"description": "nocobase telemetry library", "description": "nocobase telemetry library",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
@ -11,7 +11,7 @@
"directory": "packages/telemetry" "directory": "packages/telemetry"
}, },
"dependencies": { "dependencies": {
"@nocobase/utils": "1.7.0-beta.16", "@nocobase/utils": "1.7.0-beta.18",
"@opentelemetry/api": "^1.7.0", "@opentelemetry/api": "^1.7.0",
"@opentelemetry/instrumentation": "^0.46.0", "@opentelemetry/instrumentation": "^0.46.0",
"@opentelemetry/resources": "^1.19.0", "@opentelemetry/resources": "^1.19.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/test", "name": "@nocobase/test",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"main": "lib/index.js", "main": "lib/index.js",
"module": "./src/index.ts", "module": "./src/index.ts",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
@ -51,7 +51,7 @@
}, },
"dependencies": { "dependencies": {
"@faker-js/faker": "8.1.0", "@faker-js/faker": "8.1.0",
"@nocobase/server": "1.7.0-beta.16", "@nocobase/server": "1.7.0-beta.18",
"@playwright/test": "^1.45.3", "@playwright/test": "^1.45.3",
"@testing-library/jest-dom": "^6.4.2", "@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/utils", "name": "@nocobase/utils",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"main": "lib/index.js", "main": "lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "AGPL-3.0", "license": "AGPL-3.0",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "权限控制", "displayName.zh-CN": "权限控制",
"description": "Based on roles, resources, and actions, access control can precisely manage interface configuration permissions, data operation permissions, menu access permissions, and plugin permissions.", "description": "Based on roles, resources, and actions, access control can precisely manage interface configuration permissions, data operation permissions, menu access permissions, and plugin permissions.",
"description.zh-CN": "基于角色、资源和操作的权限控制,可以精确控制界面配置权限、数据操作权限、菜单访问权限、插件权限。", "description.zh-CN": "基于角色、资源和操作的权限控制,可以精确控制界面配置权限、数据操作权限、菜单访问权限、插件权限。",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/acl", "homepage": "https://docs.nocobase.com/handbook/acl",

View File

@ -48,9 +48,9 @@ export async function setCurrentRole(ctx: Context, next) {
const userRoles = Array.from(rolesMap.values()); const userRoles = Array.from(rolesMap.values());
ctx.state.currentUser.roles = userRoles; ctx.state.currentUser.roles = userRoles;
const systemSettings = (await cache.wrap(`app:systemSettings`, () => const systemSettings = (await cache.wrap(`app:systemSettings`, () =>
ctx.db.getRepository('systemSettings').findOne(), ctx.db.getRepository('systemSettings').findOne({ raw: true }),
)) as Model; )) as Model;
const roleMode = systemSettings?.get('roleMode') || SystemRoleMode.default; const roleMode = systemSettings?.roleMode || SystemRoleMode.default;
if ([currentRole, ctx.state.currentRole].includes(UNION_ROLE_KEY) && roleMode === SystemRoleMode.default) { if ([currentRole, ctx.state.currentRole].includes(UNION_ROLE_KEY) && roleMode === SystemRoleMode.default) {
currentRole = userRoles[0].name; currentRole = userRoles[0].name;
ctx.state.currentRole = userRoles[0].name; ctx.state.currentRole = userRoles[0].name;

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-action-bulk-edit", "name": "@nocobase/plugin-action-bulk-edit",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-bulk-edit", "homepage": "https://docs.nocobase.com/handbook/action-bulk-edit",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-bulk-edit", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-bulk-edit",

View File

@ -21,9 +21,10 @@ import {
SecondConFirm, SecondConFirm,
AfterSuccess, AfterSuccess,
RefreshDataBlockRequest, RefreshDataBlockRequest,
SchemaSettingsLinkageRules,
useDataBlockProps,
} from '@nocobase/client'; } from '@nocobase/client';
import { ModalProps } from 'antd'; import { ModalProps } from 'antd';
import { isValid } from '@formily/shared';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -96,6 +97,16 @@ export const deprecatedBulkEditActionSettings = new SchemaSettings({
return buttonEditorProps; return buttonEditorProps;
}, },
}, },
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{ {
name: 'openMode', name: 'openMode',
Component: SchemaInitializerOpenModeSchemaItems, Component: SchemaInitializerOpenModeSchemaItems,
@ -138,6 +149,16 @@ export const bulkEditActionSettings = new SchemaSettings({
return buttonEditorProps; return buttonEditorProps;
}, },
}, },
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{ {
name: 'openMode', name: 'openMode',
Component: SchemaInitializerOpenModeSchemaItems, Component: SchemaInitializerOpenModeSchemaItems,
@ -158,6 +179,7 @@ export const bulkEditActionSettings = new SchemaSettings({
name: 'updateMode', name: 'updateMode',
Component: UpdateMode, Component: UpdateMode,
}, },
{ {
name: 'remove', name: 'remove',
sort: 100, sort: 100,
@ -191,6 +213,17 @@ export const bulkEditFormSubmitActionSettings = new SchemaSettings({
name: 'afterSuccessfulSubmission', name: 'afterSuccessfulSubmission',
Component: AfterSuccess, Component: AfterSuccess,
}, },
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{ {
name: 'refreshDataBlockRequest', name: 'refreshDataBlockRequest',
Component: RefreshDataBlockRequest, Component: RefreshDataBlockRequest,

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-action-bulk-update", "name": "@nocobase/plugin-action-bulk-update",
"version": "1.7.0-beta.16", "version": "1.7.0-beta.18",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-bulk-update", "homepage": "https://docs.nocobase.com/handbook/action-bulk-update",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-bulk-update", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-bulk-update",

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