Merge branch 'develop' of github.com:nocobase/nocobase into feat-main-datasource-mssql

This commit is contained in:
aaaaaajie 2025-03-26 02:17:56 +08:00
commit b93d3bc01d
161 changed files with 2602 additions and 2050 deletions

View File

@ -5,6 +5,25 @@ 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.10](https://github.com/nocobase/nocobase/compare/v1.6.9...v1.6.10) - 2025-03-25
### 🐛 Bug Fixes
- **[client]**
- Unable to use 'Current User' variable when adding a link page ([#6536](https://github.com/nocobase/nocobase/pull/6536)) by @zhangzhonghe
- field assignment with null value is ineffective ([#6549](https://github.com/nocobase/nocobase/pull/6549)) by @katherinehhh
- `yarn doc` command error ([#6540](https://github.com/nocobase/nocobase/pull/6540)) by @gchust
- Remove the 'Allow multiple selection' option from dropdown single-select fields in filter forms ([#6515](https://github.com/nocobase/nocobase/pull/6515)) by @zhangzhonghe
- Relational field's data range linkage is not effective ([#6530](https://github.com/nocobase/nocobase/pull/6530)) by @zhangzhonghe
- **[Collection: Tree]** Migration issue for plugin-collection-tree ([#6537](https://github.com/nocobase/nocobase/pull/6537)) by @2013xile
- **[Action: Custom request]** Unable to download UTF-8 encoded files ([#6541](https://github.com/nocobase/nocobase/pull/6541)) by @2013xile
## [v1.6.9](https://github.com/nocobase/nocobase/compare/v1.6.8...v1.6.9) - 2025-03-23 ## [v1.6.9](https://github.com/nocobase/nocobase/compare/v1.6.8...v1.6.9) - 2025-03-23
### 🐛 Bug Fixes ### 🐛 Bug Fixes

View File

@ -5,6 +5,25 @@
格式基于 [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.10](https://github.com/nocobase/nocobase/compare/v1.6.9...v1.6.10) - 2025-03-25
### 🐛 修复
- **[client]**
- 添加链接页面时,无法使用“当前用户”变量 ([#6536](https://github.com/nocobase/nocobase/pull/6536)) by @zhangzhonghe
- 字段赋值对字段进行“空值”赋值无效 ([#6549](https://github.com/nocobase/nocobase/pull/6549)) by @katherinehhh
- `yarn doc` 命令报错 ([#6540](https://github.com/nocobase/nocobase/pull/6540)) by @gchust
- 筛选表单中,移除下拉单选字段的“允许多选”选项 ([#6515](https://github.com/nocobase/nocobase/pull/6515)) by @zhangzhonghe
- 关系字段的数据范围联动不生效 ([#6530](https://github.com/nocobase/nocobase/pull/6530)) by @zhangzhonghe
- **[数据表:树]** 树表插件的迁移脚本问题 ([#6537](https://github.com/nocobase/nocobase/pull/6537)) by @2013xile
- **[操作:自定义请求]** 无法下载utf8编码的文件 ([#6541](https://github.com/nocobase/nocobase/pull/6541)) by @2013xile
## [v1.6.9](https://github.com/nocobase/nocobase/compare/v1.6.8...v1.6.9) - 2025-03-23 ## [v1.6.9](https://github.com/nocobase/nocobase/compare/v1.6.8...v1.6.9) - 2025-03-23
### 🐛 修复 ### 🐛 修复

View File

@ -1,5 +1,5 @@
{ {
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/resourcer": "1.7.0-beta.9",
"@nocobase/utils": "1.7.0-alpha.4", "@nocobase/utils": "1.7.0-beta.9",
"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-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/cache": "1.7.0-beta.9",
"@nocobase/database": "1.7.0-alpha.4", "@nocobase/database": "1.7.0-beta.9",
"@nocobase/resourcer": "1.7.0-alpha.4" "@nocobase/resourcer": "1.7.0-beta.9"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,17 +1,17 @@
{ {
"name": "@nocobase/app", "name": "@nocobase/app",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/database": "1.7.0-beta.9",
"@nocobase/preset-nocobase": "1.7.0-alpha.4", "@nocobase/preset-nocobase": "1.7.0-beta.9",
"@nocobase/server": "1.7.0-alpha.4" "@nocobase/server": "1.7.0-beta.9"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/client": "1.7.0-alpha.4" "@nocobase/client": "1.7.0-beta.9"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,16 +1,16 @@
{ {
"name": "@nocobase/auth", "name": "@nocobase/auth",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/actions": "1.7.0-beta.9",
"@nocobase/cache": "1.7.0-alpha.4", "@nocobase/cache": "1.7.0-beta.9",
"@nocobase/database": "1.7.0-alpha.4", "@nocobase/database": "1.7.0-beta.9",
"@nocobase/resourcer": "1.7.0-alpha.4", "@nocobase/resourcer": "1.7.0-beta.9",
"@nocobase/utils": "1.7.0-alpha.4", "@nocobase/utils": "1.7.0-beta.9",
"@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-alpha.4", "version": "1.7.0-beta.9",
"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",

View File

@ -1,12 +1,12 @@
{ {
"name": "@nocobase/cache", "name": "@nocobase/cache",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/lock-manager": "1.7.0-beta.9",
"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-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/app": "1.7.0-beta.9",
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@umijs/utils": "3.5.20", "@umijs/utils": "3.5.20",
"chalk": "^4.1.1", "chalk": "^4.1.1",
@ -25,7 +25,7 @@
"tsx": "^4.19.0" "tsx": "^4.19.0"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/devtools": "1.7.0-alpha.4" "@nocobase/devtools": "1.7.0-beta.9"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/client", "name": "@nocobase/client",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/evaluators": "1.7.0-beta.9",
"@nocobase/sdk": "1.7.0-alpha.4", "@nocobase/sdk": "1.7.0-beta.9",
"@nocobase/utils": "1.7.0-alpha.4", "@nocobase/utils": "1.7.0-beta.9",
"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

@ -102,44 +102,46 @@ export const SettingsCenterConfigure = () => {
expandable={{ expandable={{
defaultExpandAllRows: true, defaultExpandAllRows: true,
}} }}
columns={[ columns={
{ [
dataIndex: 'title', {
title: t('Plugin name'), dataIndex: 'title',
render: (value) => { title: t('Plugin name'),
return compile(value); render: (value) => {
return compile(value);
},
}, },
}, {
{ dataIndex: 'accessible',
dataIndex: 'accessible', title: (
title: ( <>
<> <Checkbox
<Checkbox checked={allChecked}
checked={allChecked} onChange={async () => {
onChange={async () => { const values = allAclSnippets.map((v) => '!' + v);
const values = allAclSnippets.map((v) => '!' + v); if (!allChecked) {
if (!allChecked) { await resource.remove({
await resource.remove({ values,
values, });
}); } else {
} else { await resource.add({
await resource.add({ values,
values, });
}); }
} refresh();
refresh(); message.success(t('Saved successfully'));
message.success(t('Saved successfully')); }}
}} />{' '}
/>{' '} {t('Accessible')}
{t('Accessible')} </>
</> ),
), render: (_, record) => {
render: (_, record) => { const checked = !snippets.includes('!' + record.aclSnippet);
const checked = !snippets.includes('!' + record.aclSnippet); return <Checkbox checked={checked} onChange={() => handleChange(checked, record)} />;
return <Checkbox checked={checked} onChange={() => handleChange(checked, record)} />; },
}, },
}, ] as TableProps['columns']
] as TableProps['columns']} }
dataSource={settings dataSource={settings
.filter((v) => { .filter((v) => {
return v.isTopLevel !== false; return v.isTopLevel !== false;

View File

@ -121,40 +121,42 @@ export const MenuConfigure = () => {
expandable={{ expandable={{
defaultExpandAllRows: true, defaultExpandAllRows: true,
}} }}
columns={[ columns={
{ [
dataIndex: 'title', {
title: t('Menu item title'), dataIndex: 'title',
}, title: t('Menu item title'),
{
dataIndex: 'accessible',
title: (
<>
<Checkbox
checked={allChecked}
onChange={async (value) => {
if (allChecked) {
await resource.set({
values: [],
});
} else {
await resource.set({
values: allUids,
});
}
refresh();
message.success(t('Saved successfully'));
}}
/>{' '}
{t('Accessible')}
</>
),
render: (_, schema: { uid: string }) => {
const checked = uids.includes(schema.uid);
return <Checkbox checked={checked} onChange={() => handleChange(checked, schema)} />;
}, },
}, {
] as TableProps['columns']} dataIndex: 'accessible',
title: (
<>
<Checkbox
checked={allChecked}
onChange={async (value) => {
if (allChecked) {
await resource.set({
values: [],
});
} else {
await resource.set({
values: allUids,
});
}
refresh();
message.success(t('Saved successfully'));
}}
/>{' '}
{t('Accessible')}
</>
),
render: (_, schema: { uid: string }) => {
const checked = uids.includes(schema.uid);
return <Checkbox checked={checked} onChange={() => handleChange(checked, schema)} />;
},
},
] as TableProps['columns']
}
dataSource={translateTitle(items)} dataSource={translateTitle(items)}
/> />
); );

View File

@ -105,48 +105,50 @@ export const RolesResourcesActions = connect((props) => {
className={antTableCell} className={antTableCell}
size={'small'} size={'small'}
pagination={false} pagination={false}
columns={[ columns={
{ [
dataIndex: 'displayName', {
title: t('Action display name'), dataIndex: 'displayName',
render: (value) => compile(value), title: t('Action display name'),
}, render: (value) => compile(value),
{ },
dataIndex: 'onNewRecord', {
title: t('Action type'), dataIndex: 'onNewRecord',
render: (onNewRecord) => title: t('Action type'),
onNewRecord ? ( render: (onNewRecord) =>
<Tag color={'green'}>{t('Action on new records')}</Tag> onNewRecord ? (
) : ( <Tag color={'green'}>{t('Action on new records')}</Tag>
<Tag color={'geekblue'}>{t('Action on existing records')}</Tag> ) : (
), <Tag color={'geekblue'}>{t('Action on existing records')}</Tag>
}, ),
{ },
dataIndex: 'enabled', {
title: t('Allow'), dataIndex: 'enabled',
render: (enabled, action) => ( title: t('Allow'),
<Checkbox render: (enabled, action) => (
checked={enabled} <Checkbox
onChange={() => { checked={enabled}
toggleAction(action.name); onChange={() => {
}} toggleAction(action.name);
/>
),
},
{
dataIndex: 'scope',
title: t('Data scope'),
render: (value, action) =>
!action.onNewRecord && (
<ScopeSelect
value={value}
onChange={(scope) => {
setScope(action.name, scope);
}} }}
/> />
), ),
}, },
] as TableProps['columns']} {
dataIndex: 'scope',
title: t('Data scope'),
render: (value, action) =>
!action.onNewRecord && (
<ScopeSelect
value={value}
onChange={(scope) => {
setScope(action.name, scope);
}}
/>
),
},
] as TableProps['columns']
}
dataSource={availableActions?.map((item) => { dataSource={availableActions?.map((item) => {
let enabled = false; let enabled = false;
let scope = null; let scope = null;
@ -169,60 +171,62 @@ export const RolesResourcesActions = connect((props) => {
className={antTableCell} className={antTableCell}
pagination={false} pagination={false}
dataSource={fieldPermissions} dataSource={fieldPermissions}
columns={[ columns={
{ [
dataIndex: ['uiSchema', 'title'], {
title: t('Field display name'), dataIndex: ['uiSchema', 'title'],
render: (value) => compile(value), title: t('Field display name'),
}, render: (value) => compile(value),
...availableActionsWithFields.map((action) => { },
const checked = allChecked?.[action.name]; ...availableActionsWithFields.map((action) => {
return { const checked = allChecked?.[action.name];
dataIndex: action.name, return {
title: ( dataIndex: action.name,
<> title: (
<>
<Checkbox
checked={checked}
onChange={() => {
const item = actionMap[action.name] || {
name: action.name,
};
if (checked) {
item.fields = [];
} else {
item.fields = collectionFields?.map?.((item) => item.name);
}
actionMap[action.name] = item;
onChange(Object.values(actionMap));
}}
/>{' '}
{compile(action.displayName)}
</>
),
render: (checked, field) => (
<Checkbox <Checkbox
checked={checked} checked={checked}
aria-label={`${action.name}_checkbox`}
onChange={() => { onChange={() => {
const item = actionMap[action.name] || { const item = actionMap[action.name] || {
name: action.name, name: action.name,
}; };
const fields: string[] = item.fields || [];
if (checked) { if (checked) {
item.fields = []; const index = fields.indexOf(field.name);
fields.splice(index, 1);
} else { } else {
item.fields = collectionFields?.map?.((item) => item.name); fields.push(field.name);
} }
item.fields = fields;
actionMap[action.name] = item; actionMap[action.name] = item;
onChange(Object.values(actionMap)); onChange(Object.values(actionMap));
}} }}
/>{' '} />
{compile(action.displayName)} ),
</> };
), }),
render: (checked, field) => ( ] as TableProps['columns']
<Checkbox }
checked={checked}
aria-label={`${action.name}_checkbox`}
onChange={() => {
const item = actionMap[action.name] || {
name: action.name,
};
const fields: string[] = item.fields || [];
if (checked) {
const index = fields.indexOf(field.name);
fields.splice(index, 1);
} else {
fields.push(field.name);
}
item.fields = fields;
actionMap[action.name] = item;
onChange(Object.values(actionMap));
}}
/>
),
};
}),
] as TableProps['columns']}
/> />
</FormItem> </FormItem>
</FormLayout> </FormLayout>

View File

@ -55,62 +55,64 @@ export const StrategyActions = connect((props) => {
size={'small'} size={'small'}
pagination={false} pagination={false}
rowKey={'name'} rowKey={'name'}
columns={[ columns={
{ [
dataIndex: 'displayName', {
title: t('Action display name'), dataIndex: 'displayName',
render: (value) => compile(value), title: t('Action display name'),
}, render: (value) => compile(value),
{ },
dataIndex: 'onNewRecord', {
title: t('Action type'), dataIndex: 'onNewRecord',
render: (onNewRecord) => title: t('Action type'),
onNewRecord ? ( render: (onNewRecord) =>
<Tag color={'green'}>{t('Action on new records')}</Tag> onNewRecord ? (
) : ( <Tag color={'green'}>{t('Action on new records')}</Tag>
<Tag color={'geekblue'}>{t('Action on existing records')}</Tag> ) : (
), <Tag color={'geekblue'}>{t('Action on existing records')}</Tag>
}, ),
{ },
dataIndex: 'enabled', {
title: t('Allow'), dataIndex: 'enabled',
render: (enabled, action) => ( title: t('Allow'),
<Checkbox render: (enabled, action) => (
checked={enabled} <Checkbox
aria-label={`${action.name}_checkbox`} checked={enabled}
onChange={(e) => { aria-label={`${action.name}_checkbox`}
if (enabled) { onChange={(e) => {
delete scopes[action.name]; if (enabled) {
} else { delete scopes[action.name];
scopes[action.name] = 'all'; } else {
} scopes[action.name] = 'all';
onChange(toFieldValue(scopes)); }
}}
/>
),
},
{
dataIndex: 'scope',
title: t('Data scope'),
render: (scope, action) =>
!action.onNewRecord && (
<Select
data-testid="select-data-scope"
popupMatchSelectWidth={false}
size={'small'}
value={scope}
options={[
{ label: t('All records'), value: 'all' },
{ label: t('Own records'), value: 'own' },
]}
onChange={(value) => {
scopes[action.name] = value;
onChange(toFieldValue(scopes)); onChange(toFieldValue(scopes));
}} }}
/> />
), ),
}, },
] as TableProps['columns']} {
dataIndex: 'scope',
title: t('Data scope'),
render: (scope, action) =>
!action.onNewRecord && (
<Select
data-testid="select-data-scope"
popupMatchSelectWidth={false}
size={'small'}
value={scope}
options={[
{ label: t('All records'), value: 'all' },
{ label: t('Own records'), value: 'own' },
]}
onChange={(value) => {
scopes[action.name] = value;
onChange(toFieldValue(scopes));
}}
/>
),
},
] as TableProps['columns']
}
dataSource={availableActions?.map((item) => { dataSource={availableActions?.map((item) => {
let scope = 'all'; let scope = 'all';
let enabled = false; let enabled = false;

View File

@ -167,7 +167,7 @@ export function useCollectValuesToSubmit() {
if (parsedValue !== null && parsedValue !== undefined) { if (parsedValue !== null && parsedValue !== undefined) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
} }
}); });
@ -203,6 +203,12 @@ export function useCollectValuesToSubmit() {
]); ]);
} }
function interpolateVariables(str: string, scope: Record<string, any>): string {
return str.replace(/\{\{\s*([a-zA-Z0-9_$-.]+?)\s*\}\}/g, (_, key) => {
return scope[key] !== undefined ? String(scope[key]) : '';
});
}
export const useCreateActionProps = () => { export const useCreateActionProps = () => {
const filterByTk = useFilterByTk(); const filterByTk = useFilterByTk();
const record = useCollectionRecord(); const record = useCollectionRecord();
@ -219,11 +225,20 @@ export const useCreateActionProps = () => {
const collectValues = useCollectValuesToSubmit(); const collectValues = useCollectValuesToSubmit();
const action = record.isNew ? actionField.componentProps.saveMode || 'create' : 'update'; const action = record.isNew ? actionField.componentProps.saveMode || 'create' : 'update';
const filterKeys = actionField.componentProps.filterKeys?.checked || []; const filterKeys = actionField.componentProps.filterKeys?.checked || [];
const localVariables = useLocalVariables();
const variables = useVariables();
return { return {
async onClick() { async onClick() {
const { onSuccess, skipValidator, triggerWorkflows } = actionSchema?.['x-action-settings'] ?? {}; const { onSuccess, skipValidator, triggerWorkflows } = actionSchema?.['x-action-settings'] ?? {};
const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {}; const {
manualClose,
redirecting,
redirectTo: rawRedirectTo,
successMessage,
actionAfterSuccess,
} = onSuccess || {};
if (!skipValidator) { if (!skipValidator) {
await form.submit(); await form.submit();
} }
@ -241,6 +256,15 @@ export const useCreateActionProps = () => {
: undefined, : undefined,
updateAssociationValues, updateAssociationValues,
}); });
let redirectTo = rawRedirectTo;
if (rawRedirectTo) {
const { exp, scope: expScope } = await replaceVariables(rawRedirectTo, {
variables,
localVariables: [...localVariables, { name: '$record', ctx: new Proxy(data?.data?.data, {}) }],
});
redirectTo = interpolateVariables(exp, expScope);
}
if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) { if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {
setVisible?.(false); setVisible?.(false);
} }
@ -338,7 +362,7 @@ export const useAssociationCreateActionProps = () => {
if (parsedValue) { if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
} }
}); });
@ -588,7 +612,13 @@ export const useCustomizeUpdateActionProps = () => {
skipValidator, skipValidator,
triggerWorkflows, triggerWorkflows,
} = actionSchema?.['x-action-settings'] ?? {}; } = actionSchema?.['x-action-settings'] ?? {};
const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {}; const {
manualClose,
redirecting,
redirectTo: rawRedirectTo,
successMessage,
actionAfterSuccess,
} = onSuccess || {};
const assignedValues = {}; const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => { const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key]; const value = originalAssignedValues[key];
@ -605,7 +635,7 @@ export const useCustomizeUpdateActionProps = () => {
if (parsedValue) { if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
} }
}); });
@ -614,7 +644,7 @@ export const useCustomizeUpdateActionProps = () => {
if (skipValidator === false) { if (skipValidator === false) {
await form.submit(); await form.submit();
} }
await resource.update({ const result = await resource.update({
filterByTk, filterByTk,
values: { ...assignedValues }, values: { ...assignedValues },
// TODO(refactor): should change to inject by plugin // TODO(refactor): should change to inject by plugin
@ -622,6 +652,16 @@ export const useCustomizeUpdateActionProps = () => {
? triggerWorkflows.map((row) => [row.workflowKey, row.context].filter(Boolean).join('!')).join(',') ? triggerWorkflows.map((row) => [row.workflowKey, row.context].filter(Boolean).join('!')).join(',')
: undefined, : undefined,
}); });
let redirectTo = rawRedirectTo;
if (rawRedirectTo) {
const { exp, scope: expScope } = await replaceVariables(rawRedirectTo, {
variables,
localVariables: [...localVariables, { name: '$record', ctx: new Proxy(result?.data?.data?.[0], {}) }],
});
redirectTo = interpolateVariables(exp, expScope);
}
if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) { if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {
setVisible?.(false); setVisible?.(false);
} }
@ -708,7 +748,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
if (parsedValue) { if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
} }
}); });
@ -913,7 +953,13 @@ export const useUpdateActionProps = () => {
skipValidator, skipValidator,
triggerWorkflows, triggerWorkflows,
} = actionSchema?.['x-action-settings'] ?? {}; } = actionSchema?.['x-action-settings'] ?? {};
const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {}; const {
manualClose,
redirecting,
redirectTo: rawRedirectTo,
successMessage,
actionAfterSuccess,
} = onSuccess || {};
const assignedValues = {}; const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => { const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key]; const value = originalAssignedValues[key];
@ -930,7 +976,7 @@ export const useUpdateActionProps = () => {
if (parsedValue) { if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
} }
}); });
@ -952,7 +998,7 @@ export const useUpdateActionProps = () => {
actionField.data = field.data || {}; actionField.data = field.data || {};
actionField.data.loading = true; actionField.data.loading = true;
try { try {
await resource.update({ const result = await resource.update({
filterByTk, filterByTk,
values: { values: {
...values, ...values,
@ -971,6 +1017,15 @@ export const useUpdateActionProps = () => {
if (callBack) { if (callBack) {
callBack?.(); callBack?.();
} }
let redirectTo = rawRedirectTo;
if (rawRedirectTo) {
const { exp, scope: expScope } = await replaceVariables(rawRedirectTo, {
variables,
localVariables: [...localVariables, { name: '$record', ctx: new Proxy(result?.data?.data?.[0], {}) }],
});
redirectTo = interpolateVariables(exp, expScope);
}
if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) { if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {
setVisible?.(false); setVisible?.(false);
} }

View File

@ -157,6 +157,8 @@ export const useCollectionFilterOptions = (collection: any, dataSource?: string)
const option = { const option = {
name: field.name, name: field.name,
title: field?.uiSchema?.title || field.name, title: field?.uiSchema?.title || field.name,
label: field?.uiSchema?.title || field.name,
value: field.name,
schema: field?.uiSchema, schema: field?.uiSchema,
operators: operators:
operators?.filter?.((operator) => { operators?.filter?.((operator) => {

File diff suppressed because it is too large Load Diff

View File

@ -1094,5 +1094,6 @@
"Font Sizepx": "字体大小(像素)", "Font Sizepx": "字体大小(像素)",
"Font Weight": "字体粗细", "Font Weight": "字体粗细",
"Font Style": "字体样式", "Font Style": "字体样式",
"Italic": "斜体" "Italic": "斜体",
"Response record":"响应结果记录"
} }

View File

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

View File

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

View File

@ -127,25 +127,27 @@ function BulkEnableButton({ plugins = [] }) {
}} }}
size={'small'} size={'small'}
pagination={false} pagination={false}
columns={[ columns={
{ [
title: t('Plugin'), {
dataIndex: 'displayName', title: t('Plugin'),
ellipsis: true, dataIndex: 'displayName',
}, ellipsis: true,
{ },
title: t('Description'), {
dataIndex: 'description', title: t('Description'),
ellipsis: true, dataIndex: 'description',
width: 300, ellipsis: true,
}, width: 300,
{ },
title: t('Package name'), {
dataIndex: 'packageName', title: t('Package name'),
width: 300, dataIndex: 'packageName',
ellipsis: true, width: 300,
}, ellipsis: true,
] as TableProps['columns']} },
] as TableProps['columns']
}
dataSource={items} dataSource={items}
/> />
</Modal> </Modal>

View File

@ -88,7 +88,7 @@ export const SettingsCenterDropdown = () => {
<Button <Button
data-testid="plugin-settings-button" data-testid="plugin-settings-button"
icon={<SettingOutlined style={{ color: token.colorTextHeaderMenu }} />} icon={<SettingOutlined style={{ color: token.colorTextHeaderMenu }} />}
// title={t('All plugin settings')} // title={t('All plugin settings')}
/> />
</Dropdown> </Dropdown>
); );

View File

@ -52,6 +52,7 @@ import { KeepAlive } from './KeepAlive';
import { NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema'; import { NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema';
import { MenuSchemaToolbar, ResetThemeTokenAndKeepAlgorithm } from './menuItemSettings'; import { MenuSchemaToolbar, ResetThemeTokenAndKeepAlgorithm } from './menuItemSettings';
import { userCenterSettings } from './userCenterSettings'; import { userCenterSettings } from './userCenterSettings';
import { createGlobalStyle, createStyles } from 'antd-style';
export { KeepAlive, NocoBaseDesktopRouteType }; export { KeepAlive, NocoBaseDesktopRouteType };
@ -268,7 +269,7 @@ const GroupItem: FC<{ item: any }> = (props) => {
}; };
const WithTooltip: FC<{ title: string; hidden: boolean }> = (props) => { const WithTooltip: FC<{ title: string; hidden: boolean }> = (props) => {
const { inHeader } = useContext(headerContext); const { inHeader } = useContext(HeaderContext);
return ( return (
<RouteContext.Consumer> <RouteContext.Consumer>
@ -406,7 +407,7 @@ const contentStyle = {
paddingInline: 0, paddingInline: 0,
}; };
const headerContext = React.createContext<{ inHeader: boolean }>({ inHeader: false }); const HeaderContext = React.createContext<{ inHeader: boolean }>({ inHeader: false });
const popoverStyle = css` const popoverStyle = css`
.ant-popover-inner { .ant-popover-inner {
@ -495,9 +496,38 @@ const collapsedButtonRender = (collapsed, dom) => {
return <CollapsedButton collapsed={collapsed}>{dom}</CollapsedButton>; return <CollapsedButton collapsed={collapsed}>{dom}</CollapsedButton>;
}; };
/**
* antd bug antd
* - issue: https://github.com/ant-design/pro-components/issues/8593
* - issue: https://github.com/ant-design/pro-components/issues/8522
* - issue: https://github.com/ant-design/pro-components/issues/8432
*/
const useHeaderStyle = createStyles(({ token }: any) => {
return {
headerPopup: {
'&.ant-menu-submenu-popup>.ant-menu': {
backgroundColor: `${token.colorBgHeader}`,
},
},
headerMenuActive: {
'& .ant-menu-submenu-selected>.ant-menu-submenu-title': {
color: token.colorTextHeaderMenuActive,
},
},
};
});
const headerContextValue = { inHeader: true }; const headerContextValue = { inHeader: true };
const HeaderWrapper: FC = (props) => {
const { styles } = useHeaderStyle();
return (
<span className={styles.headerMenuActive}>
<HeaderContext.Provider value={headerContextValue}>{props.children}</HeaderContext.Provider>
</span>
);
};
const headerRender = (props: HeaderViewProps, defaultDom: React.ReactNode) => { const headerRender = (props: HeaderViewProps, defaultDom: React.ReactNode) => {
return <headerContext.Provider value={headerContextValue}>{defaultDom}</headerContext.Provider>; return <HeaderWrapper>{defaultDom}</HeaderWrapper>;
}; };
const IsMobileLayoutContext = React.createContext<{ const IsMobileLayoutContext = React.createContext<{
@ -531,6 +561,7 @@ export const InternalAdminLayout = () => {
const doNotChangeCollapsedRef = useRef(false); const doNotChangeCollapsedRef = useRef(false);
const { t } = useMenuTranslation(); const { t } = useMenuTranslation();
const designable = isMobileLayout ? false : _designable; const designable = isMobileLayout ? false : _designable;
const { styles } = useHeaderStyle();
const route = useMemo(() => { const route = useMemo(() => {
return { return {
@ -544,7 +575,7 @@ export const InternalAdminLayout = () => {
colorBgHeader: token.colorBgHeader, colorBgHeader: token.colorBgHeader,
colorTextMenu: token.colorTextHeaderMenu, colorTextMenu: token.colorTextHeaderMenu,
colorTextMenuSelected: token.colorTextHeaderMenuActive, colorTextMenuSelected: token.colorTextHeaderMenuActive,
colorTextMenuActive: token.colorTextHeaderMenuActive, colorTextMenuActive: token.colorTextHeaderMenuHover,
colorBgMenuItemHover: token.colorBgHeaderMenuHover, colorBgMenuItemHover: token.colorBgHeaderMenuHover,
colorBgMenuItemSelected: token.colorBgHeaderMenuActive, colorBgMenuItemSelected: token.colorBgHeaderMenuActive,
heightLayoutHeader: 46, heightLayoutHeader: 46,
@ -591,6 +622,12 @@ export const InternalAdminLayout = () => {
}); });
}, []); }, []);
const menuProps = useMemo(() => {
return {
overflowedIndicatorPopupClassName: styles.headerPopup,
};
}, [styles.headerPopup]);
return ( return (
<DndContext onDragEnd={onDragEnd}> <DndContext onDragEnd={onDragEnd}>
<ProLayout <ProLayout
@ -612,6 +649,7 @@ export const InternalAdminLayout = () => {
onCollapse={onCollapse} onCollapse={onCollapse}
collapsed={collapsed} collapsed={collapsed}
onPageChange={onPageChange} onPageChange={onPageChange}
menuProps={menuProps}
> >
<RouteContext.Consumer> <RouteContext.Consumer>
{(value: RouteContextType) => { {(value: RouteContextType) => {

View File

@ -34,6 +34,8 @@ import {
import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { useLinkageAction } from './hooks'; import { useLinkageAction } from './hooks';
import { requestSettingsSchema } from './utils'; import { requestSettingsSchema } from './utils';
import { useAfterSuccessOptions } from './hooks/useGetAfterSuccessVariablesOptions';
import { useGlobalVariable } from '../../../application/hooks/useGlobalVariable';
const MenuGroup = (props) => { const MenuGroup = (props) => {
return props.children; return props.children;
@ -280,11 +282,24 @@ export function SkipValidation() {
); );
} }
const fieldNames = {
value: 'value',
label: 'label',
};
const useVariableProps = (environmentVariables) => {
const scope = useAfterSuccessOptions();
return {
scope: [environmentVariables, ...scope].filter(Boolean),
fieldNames,
};
};
export function AfterSuccess() { export function AfterSuccess() {
const { dn } = useDesignable(); const { dn } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { onSuccess } = fieldSchema?.['x-action-settings'] || {}; const { onSuccess } = fieldSchema?.['x-action-settings'] || {};
const environmentVariables = useGlobalVariable('$env');
return ( return (
<SchemaSettingsModalItem <SchemaSettingsModalItem
title={t('After successful submission')} title={t('After successful submission')}
@ -363,8 +378,9 @@ export function AfterSuccess() {
redirectTo: { redirectTo: {
title: t('Link'), title: t('Link'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input', 'x-component': 'Variable.TextArea',
'x-component-props': {}, // eslint-disable-next-line react-hooks/rules-of-hooks
'x-use-component-props': () => useVariableProps(environmentVariables),
}, },
}, },
} as ISchema } as ISchema

View File

@ -16,16 +16,19 @@ import Action from './Action';
import { ComposedAction } from './types'; import { ComposedAction } from './types';
import { Icon } from '../../../icon'; import { Icon } from '../../../icon';
const WrapperComponent = ({ component: Component = 'a', icon, onlyIcon, children, ...restProps }: any) => { const WrapperComponent = React.forwardRef(
return ( ({ component: Component = 'a', icon, onlyIcon, children, ...restProps }: any, ref) => {
<Component {...restProps}> return (
<Tooltip title={restProps.title}> <Component ref={ref} {...restProps}>
<span style={{ marginRight: 3 }}>{icon && typeof icon === 'string' ? <Icon type={icon} /> : icon}</span> <Tooltip title={restProps.title}>
</Tooltip> <span style={{ marginRight: 3 }}>{icon && typeof icon === 'string' ? <Icon type={icon} /> : icon}</span>
{onlyIcon ? children[1] : children} </Tooltip>
</Component> {onlyIcon ? children[1] : children}
); </Component>
}; );
},
);
WrapperComponent.displayName = 'WrapperComponentLink';
export const ActionLink: ComposedAction = withDynamicSchemaProps( export const ActionLink: ComposedAction = withDynamicSchemaProps(
observer((props: any) => { observer((props: any) => {
@ -34,6 +37,7 @@ export const ActionLink: ComposedAction = withDynamicSchemaProps(
{...props} {...props}
component={props.component || WrapperComponent} component={props.component || WrapperComponent}
className={classnames('nb-action-link', props.className)} className={classnames('nb-action-link', props.className)}
isLink
/> />
); );
}), }),

View File

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

View File

@ -0,0 +1,66 @@
/**
* 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 { useMemo } from 'react';
import { useCollection_deprecated, useCollectionFilterOptions } from '../../../../collection-manager';
import { useCollectionRecordData } from '../../../../data-source';
import { useTranslation } from 'react-i18next';
import { useCompile } from '../../../';
import { useBlockContext } from '../../../../block-provider/BlockProvider';
import { usePopupVariable } from '../../../../schema-settings/VariableInput/hooks';
import { useCurrentRoleVariable } from '../../../../schema-settings/VariableInput/hooks';
export const useAfterSuccessOptions = () => {
const collection = useCollection_deprecated();
const { t } = useTranslation();
const fieldsOptions = useCollectionFilterOptions(collection);
const userFieldOptions = useCollectionFilterOptions('users', 'main');
const compile = useCompile();
const recordData = useCollectionRecordData();
const { name: blockType } = useBlockContext() || {};
const [fields, userFields] = useMemo(() => {
return [compile(fieldsOptions), compile(userFieldOptions)];
}, [fieldsOptions, userFieldOptions]);
const { settings: popupRecordSettings, shouldDisplayPopupRecord } = usePopupVariable();
const { currentRoleSettings } = useCurrentRoleVariable();
const record = useCollectionRecordData();
return useMemo(() => {
return [
(record || blockType === 'form') && {
value: '$record',
label: t('Response record', { ns: 'client' }),
children: [...fields],
},
recordData && {
value: 'currentRecord',
label: t('Current record', { ns: 'client' }),
children: [...fields],
},
shouldDisplayPopupRecord && {
...popupRecordSettings,
},
{
value: 'currentUser',
label: t('Current user', { ns: 'client' }),
children: userFields,
},
currentRoleSettings,
{
value: 'currentTime',
label: t('Current time', { ns: 'client' }),
children: null,
},
{
value: '$nToken',
label: 'API token',
children: null,
},
].filter(Boolean);
}, [recordData, t, fields, blockType, userFields]);
};

View File

@ -16,5 +16,6 @@ export * from './hooks/useGetAriaLabelOfAction';
export * from './hooks/useGetAriaLabelOfDrawer'; export * from './hooks/useGetAriaLabelOfDrawer';
export * from './hooks/useGetAriaLabelOfModal'; export * from './hooks/useGetAriaLabelOfModal';
export * from './hooks/useGetAriaLabelOfPopover'; export * from './hooks/useGetAriaLabelOfPopover';
export * from './hooks/useGetAfterSuccessVariablesOptions';
export * from './types'; export * from './types';
export * from './zIndexContext'; export * from './zIndexContext';

View File

@ -14,22 +14,22 @@ import { uid } from '@formily/shared';
import { Space, message } from 'antd'; import { Space, message } from 'antd';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { isFunction } from 'mathjs'; import { isFunction } from 'mathjs';
import React, { useEffect, useState, useContext } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
ClearCollectionFieldContext, ClearCollectionFieldContext,
NocoBaseRecursionField, NocoBaseRecursionField,
RecordProvider, RecordProvider,
SchemaComponentContext,
useAPIClient, useAPIClient,
useCollectionRecordData, useCollectionRecordData,
SchemaComponentContext,
} from '../../../'; } from '../../../';
import { Action } from '../action'; import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
import { isVariable } from '../../../variables/utils/isVariable'; import { isVariable } from '../../../variables/utils/isVariable';
import { getInnermostKeyAndValue } from '../../common/utils/uitls'; import { getInnermostKeyAndValue } from '../../common/utils/uitls';
import { Action } from '../action';
import { RemoteSelect, RemoteSelectProps } from '../remote-select'; import { RemoteSelect, RemoteSelectProps } from '../remote-select';
import useServiceOptions, { useAssociationFieldContext } from './hooks'; import useServiceOptions, { useAssociationFieldContext } from './hooks';
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
export const AssociationFieldAddNewer = (props) => { export const AssociationFieldAddNewer = (props) => {
const schemaComponentCtxValue = useContext(SchemaComponentContext); const schemaComponentCtxValue = useContext(SchemaComponentContext);

View File

@ -158,7 +158,10 @@ export const InternalPicker = observer(
}), }),
[], [],
); );
const newSchema = useMemo(() => isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema, [isMobileLayout, fieldSchema]); const newSchema = useMemo(
() => (isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema),
[isMobileLayout, fieldSchema],
);
return ( return (
<PopupSettingsProvider enableURL={false}> <PopupSettingsProvider enableURL={false}>

View File

@ -144,8 +144,14 @@ const ToManyNester = observer(
const update = useUpdate(); const update = useUpdate();
const { isMobileLayout } = useMobileLayout(); const { isMobileLayout } = useMobileLayout();
const newSchema = useMemo(() => isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema, [isMobileLayout, fieldSchema]); const newSchema = useMemo(
const newParentSchema = useMemo(() => isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema.parent) : fieldSchema.parent, [isMobileLayout, fieldSchema.parent]); () => (isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema),
[isMobileLayout, fieldSchema],
);
const newParentSchema = useMemo(
() => (isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema.parent) : fieldSchema.parent),
[isMobileLayout, fieldSchema.parent],
);
const refreshComponent = useRefreshComponent(); const refreshComponent = useRefreshComponent();
const refresh = useCallback(() => { const refresh = useCallback(() => {

View File

@ -60,6 +60,7 @@ export function useAssociationFieldContext<F extends GeneralField>() {
}; };
} }
// 用于获取关系字段请求数据时所需的一些参数
export default function useServiceOptions(props) { export default function useServiceOptions(props) {
const { action = 'list', service, useOriginalFilter } = props; const { action = 'list', service, useOriginalFilter } = props;
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
@ -86,24 +87,24 @@ export default function useServiceOptions(props) {
mergeFilter([ mergeFilter([
isOToAny && !isInFilterFormBlock(fieldSchema) && collectionField?.foreignKey && !useOriginalFilter isOToAny && !isInFilterFormBlock(fieldSchema) && collectionField?.foreignKey && !useOriginalFilter
? { ? {
[collectionField.foreignKey]: { [collectionField.foreignKey]: {
$is: null, $is: null,
}, },
} }
: null, : null,
parsedFilterParams, parsedFilterParams,
]), ]),
isOToAny && isOToAny &&
sourceValue !== undefined && sourceValue !== undefined &&
sourceValue !== null && sourceValue !== null &&
!isInFilterFormBlock(fieldSchema) && !isInFilterFormBlock(fieldSchema) &&
collectionField?.foreignKey && collectionField?.foreignKey &&
!useOriginalFilter !useOriginalFilter
? { ? {
[collectionField.foreignKey]: { [collectionField.foreignKey]: {
$eq: sourceValue, $eq: sourceValue,
}, },
} }
: null, : null,
// params?.filter && value?.length // params?.filter && value?.length
// ? { // ? {

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-11aiz3o" class="nb-block-item nb-form-item css-9qorhu 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-11aiz3o" class="nb-block-item nb-form-item css-9qorhu ant-nb-block-item css-dev-only-do-not-override-1rquknz"
role="button" role="button"
> >
<div <div

View File

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

View File

@ -52,7 +52,10 @@ const FormComponent: React.FC<FormProps> = (props) => {
labelWrap = true, labelWrap = true,
} = cardItemSchema?.['x-component-props'] || {}; } = cardItemSchema?.['x-component-props'] || {};
const { isMobileLayout } = useMobileLayout(); const { isMobileLayout } = useMobileLayout();
const newSchema = useMemo(() => isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema, [fieldSchema, isMobileLayout]); const newSchema = useMemo(
() => (isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema),
[fieldSchema, isMobileLayout],
);
return ( return (
<FieldContext.Provider value={undefined}> <FieldContext.Provider value={undefined}>
@ -81,12 +84,7 @@ const FormComponent: React.FC<FormProps> = (props) => {
} }
`} `}
> >
<NocoBaseRecursionField <NocoBaseRecursionField basePath={f.address} schema={newSchema} onlyRenderProperties isUseFormilyField />
basePath={f.address}
schema={newSchema}
onlyRenderProperties
isUseFormilyField
/>
</div> </div>
</FormLayout> </FormLayout>
</FormContext.Provider> </FormContext.Provider>
@ -104,19 +102,17 @@ const FormDecorator: React.FC<FormProps> = (props) => {
const f = useAttach(form.createVoidField({ ...field.props, basePath: '' })); const f = useAttach(form.createVoidField({ ...field.props, basePath: '' }));
const Component = useComponent(fieldSchema['x-component'], Def); const Component = useComponent(fieldSchema['x-component'], Def);
const { isMobileLayout } = useMobileLayout(); const { isMobileLayout } = useMobileLayout();
const newSchema = useMemo(() => isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema, [fieldSchema, isMobileLayout]); const newSchema = useMemo(
() => (isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema),
[fieldSchema, isMobileLayout],
);
return ( return (
<FieldContext.Provider value={undefined}> <FieldContext.Provider value={undefined}>
<FormContext.Provider value={form}> <FormContext.Provider value={form}>
<FormLayout layout={'vertical'} {...others}> <FormLayout layout={'vertical'} {...others}>
<FieldContext.Provider value={f}> <FieldContext.Provider value={f}>
<Component {...field.componentProps}> <Component {...field.componentProps}>
<NocoBaseRecursionField <NocoBaseRecursionField basePath={f.address} schema={newSchema} onlyRenderProperties isUseFormilyField />
basePath={f.address}
schema={newSchema}
onlyRenderProperties
isUseFormilyField
/>
</Component> </Component>
</FieldContext.Provider> </FieldContext.Provider>
{/* <FieldContext.Provider value={f}>{children}</FieldContext.Provider> */} {/* <FieldContext.Provider value={f}>{children}</FieldContext.Provider> */}

View File

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

View File

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

View File

@ -343,14 +343,14 @@ export const TableBlockDesigner = () => {
}} }}
/> />
<SchemaSettingsConnectDataBlocks type={FilterBlockType.TABLE} emptyDescription={t('No blocks to connect')} /> <SchemaSettingsConnectDataBlocks type={FilterBlockType.TABLE} emptyDescription={t('No blocks to connect')} />
{supportTemplate && <SchemaSettingsDivider />} {/* {supportTemplate && <SchemaSettingsDivider />}
{supportTemplate && ( {supportTemplate && (
<SchemaSettingsTemplate <SchemaSettingsTemplate
componentName={`${componentNamePrefix}Table`} componentName={`${componentNamePrefix}Table`}
collectionName={name} collectionName={name}
resourceName={defaultResource} resourceName={defaultResource}
/> />
)} )} */}
<SchemaSettingsDivider /> <SchemaSettingsDivider />
<SchemaSettingsRemove <SchemaSettingsRemove
removeParentsIfNoChildren removeParentsIfNoChildren

View File

@ -233,10 +233,10 @@ describe('Table.settings', () => {
}, },
], ],
}, },
{ // {
title: 'Save as template', // title: 'Save as template',
type: 'modal', // type: 'modal',
}, // },
{ {
title: 'Delete', title: 'Delete',
type: 'delete', type: 'delete',
@ -299,7 +299,12 @@ describe('Table.settings', () => {
test('menu list', async () => { test('menu list', async () => {
await renderSettings(getRenderSettingsOptions()); await renderSettings(getRenderSettingsOptions());
await checkTableSettings(); await checkTableSettings([
{
title: 'Save as template',
type: 'modal',
},
]);
}); });
test('old schema', async () => { test('old schema', async () => {

View File

@ -45,9 +45,9 @@ export const Sortable = (props: any) => {
const { draggable, droppable } = useContext(SortableContext); const { draggable, droppable } = useContext(SortableContext);
const { isOver, setNodeRef } = droppable; const { isOver, setNodeRef } = droppable;
const droppableStyle = { ...style }; const droppableStyle = { ...style };
const isLinkComponent = component === 'a' || component?.displayName === 'WrapperComponentLink';
if (isOver && draggable?.active?.id !== droppable?.over?.id) { if (isOver && draggable?.active?.id !== droppable?.over?.id) {
droppableStyle[component === 'a' ? 'color' : 'background'] = getComputedColor(token.colorSettings); droppableStyle[isLinkComponent ? 'color' : 'background'] = getComputedColor(token.colorSettings);
Object.assign(droppableStyle, overStyle); Object.assign(droppableStyle, overStyle);
} }
@ -118,7 +118,6 @@ export const SortableItem: React.FC<SortableItemProps> = React.memo((props) => {
if (designable) { if (designable) {
return <InternalSortableItem {...props} />; return <InternalSortableItem {...props} />;
} }
return React.createElement( return React.createElement(
component || 'div', component || 'div',
_.omit(others, ['children', 'schema', 'overStyle', 'openMode', 'id', 'eid', 'removeParentsIfNoChildren']), _.omit(others, ['children', 'schema', 'overStyle', 'openMode', 'id', 'eid', 'removeParentsIfNoChildren']),

View File

@ -267,6 +267,13 @@ function FinallyButton({
}) { }) {
const { getCollection } = useCollectionManager_deprecated(); const { getCollection } = useCollectionManager_deprecated();
const aclCtx = useACLActionParamsContext(); const aclCtx = useACLActionParamsContext();
const buttonStyle = useMemo(() => {
const shouldApplyOpacity = designable && (field?.data?.hidden || !aclCtx);
const opacityValue = componentType !== 'link' ? (shouldApplyOpacity ? 0.1 : 1) : 1;
return {
opacity: opacityValue,
};
}, [designable, field?.data?.hidden, aclCtx, componentType]);
if (inheritsCollections?.length > 0) { if (inheritsCollections?.length > 0) {
if (!linkageFromForm) { if (!linkageFromForm) {
@ -276,6 +283,7 @@ function FinallyButton({
danger={props.danger} danger={props.danger}
type={componentType} type={componentType}
icon={<DownOutlined />} icon={<DownOutlined />}
style={{ ...props?.style, ...buttonStyle }}
buttonsRender={([leftButton, rightButton]) => [ buttonsRender={([leftButton, rightButton]) => [
React.cloneElement(leftButton as React.ReactElement<any, string>, { React.cloneElement(leftButton as React.ReactElement<any, string>, {
style: props?.style, style: props?.style,
@ -296,7 +304,13 @@ function FinallyButton({
) : ( ) : (
<Dropdown menu={menu}> <Dropdown menu={menu}>
{ {
<Button aria-label={props['aria-label']} icon={icon} type={componentType} danger={props.danger}> <Button
aria-label={props['aria-label']}
icon={icon}
type={componentType}
danger={props.danger}
style={{ ...props?.style, ...buttonStyle }}
>
{props.children} <DownOutlined /> {props.children} <DownOutlined />
</Button> </Button>
} }
@ -321,6 +335,7 @@ function FinallyButton({
style={{ style={{
display: !designable && field?.data?.hidden && 'none', display: !designable && field?.data?.hidden && 'none',
opacity: designable && field?.data?.hidden && 0.1, opacity: designable && field?.data?.hidden && 0.1,
...buttonStyle,
}} }}
> >
{props.children} {props.children}
@ -342,7 +357,7 @@ function FinallyButton({
...props?.style, ...props?.style,
display: !designable && field?.data?.hidden && 'none', display: !designable && field?.data?.hidden && 'none',
opacity: designable && field?.data?.hidden && 0.1, opacity: designable && field?.data?.hidden && 0.1,
height: '100%', ...buttonStyle,
}} }}
> >
{props.onlyIcon ? props?.children?.[1] : props?.children} {props.onlyIcon ? props?.children?.[1] : props?.children}

View File

@ -57,16 +57,6 @@ export const useLinkageCollectionFieldOptions = (collectionName: string, readPre
if (nested || children || ['formula', 'richText', 'sequence'].includes(fieldInterface.name)) { if (nested || children || ['formula', 'richText', 'sequence'].includes(fieldInterface.name)) {
return operator?.value !== ActionType.Value && operator?.value !== ActionType.Options; return operator?.value !== ActionType.Value && operator?.value !== ActionType.Options;
} }
if (!['select', 'radioGroup', 'multipleSelect', 'checkboxGroup'].includes(fieldInterface.name)) {
return operator?.value !== ActionType.Options;
}
if (
!['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp', 'createdAt', 'updatedAt'].includes(
fieldInterface.name,
)
) {
return operator?.value !== ActionType.DateScope;
}
return true; return true;
}) || [], }) || [],
}; };

View File

@ -10,6 +10,7 @@
import { useField } from '@formily/react'; import { useField } from '@formily/react';
import { useEffect, useContext } from 'react'; import { useEffect, useContext } from 'react';
import { LinkageLogicContext } from './context'; import { LinkageLogicContext } from './context';
import { ActionType } from './type';
const findOption = (dataIndex = [], options) => { const findOption = (dataIndex = [], options) => {
let items = options; let items = options;
@ -45,9 +46,34 @@ export const useValues = (options) => {
field.data = field.data || {}; field.data = field.data || {};
const operators = option?.operators; const operators = option?.operators;
field.data.operators = operators?.filter((v) => { field.data.operators = operators?.filter((v) => {
if (dataIndex.length > 1) { const isOptionField = ['select', 'radioGroup', 'multipleSelect', 'checkboxGroup'].includes(
return !['value', 'dateScope', 'options'].includes(v.value); option?.interface || '',
);
const isDateField = [
'date',
'datetime',
'dateOnly',
'datetimeNoTz',
'unixTimestamp',
'createdAt',
'updatedAt',
].includes(option?.interface || '');
// 如果 多个字段,则排除 Value、DateScope、Options
if (dataIndex.length > 1 && [ActionType.Value, ActionType.DateScope, ActionType.Options].includes(v.value)) {
return false;
} }
// 非选项字段,去掉 Options
if (!isOptionField && v.value === ActionType.Options) {
return false;
}
// 非时间字段,去掉 DateScope
if (!isDateField && v.value === ActionType.DateScope) {
return false;
}
return true; return true;
}); });
field.data.schema = option?.schema; field.data.schema = option?.schema;

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "create-nocobase-app", "name": "create-nocobase-app",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"main": "src/index.js", "main": "src/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {

View File

@ -1,16 +1,16 @@
{ {
"name": "@nocobase/data-source-manager", "name": "@nocobase/data-source-manager",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/actions": "1.7.0-beta.9",
"@nocobase/cache": "1.7.0-alpha.4", "@nocobase/cache": "1.7.0-beta.9",
"@nocobase/database": "1.7.0-alpha.4", "@nocobase/database": "1.7.0-beta.9",
"@nocobase/resourcer": "1.7.0-alpha.4", "@nocobase/resourcer": "1.7.0-beta.9",
"@nocobase/utils": "1.7.0-alpha.4", "@nocobase/utils": "1.7.0-beta.9",
"@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-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/logger": "1.7.0-beta.9",
"@nocobase/utils": "1.7.0-alpha.4", "@nocobase/utils": "1.7.0-beta.9",
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"cron-parser": "4.4.0", "cron-parser": "4.4.0",

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/devtools", "name": "@nocobase/devtools",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/build": "1.7.0-beta.9",
"@nocobase/client": "1.7.0-alpha.4", "@nocobase/client": "1.7.0-beta.9",
"@nocobase/test": "1.7.0-alpha.4", "@nocobase/test": "1.7.0-beta.9",
"@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",

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/evaluators", "name": "@nocobase/evaluators",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/utils": "1.7.0-beta.9",
"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-alpha.4", "version": "1.7.0-beta.9",
"main": "lib/index.js", "main": "lib/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {
"@nocobase/utils": "1.7.0-alpha.4", "@nocobase/utils": "1.7.0-beta.9",
"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-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/utils": "1.7.0-beta.9",
"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-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/acl": "1.7.0-beta.9",
"@nocobase/actions": "1.7.0-alpha.4", "@nocobase/actions": "1.7.0-beta.9",
"@nocobase/auth": "1.7.0-alpha.4", "@nocobase/auth": "1.7.0-beta.9",
"@nocobase/cache": "1.7.0-alpha.4", "@nocobase/cache": "1.7.0-beta.9",
"@nocobase/data-source-manager": "1.7.0-alpha.4", "@nocobase/data-source-manager": "1.7.0-beta.9",
"@nocobase/database": "1.7.0-alpha.4", "@nocobase/database": "1.7.0-beta.9",
"@nocobase/evaluators": "1.7.0-alpha.4", "@nocobase/evaluators": "1.7.0-beta.9",
"@nocobase/lock-manager": "1.7.0-alpha.4", "@nocobase/lock-manager": "1.7.0-beta.9",
"@nocobase/logger": "1.7.0-alpha.4", "@nocobase/logger": "1.7.0-beta.9",
"@nocobase/resourcer": "1.7.0-alpha.4", "@nocobase/resourcer": "1.7.0-beta.9",
"@nocobase/sdk": "1.7.0-alpha.4", "@nocobase/sdk": "1.7.0-beta.9",
"@nocobase/telemetry": "1.7.0-alpha.4", "@nocobase/telemetry": "1.7.0-beta.9",
"@nocobase/utils": "1.7.0-alpha.4", "@nocobase/utils": "1.7.0-beta.9",
"@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",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/telemetry", "name": "@nocobase/telemetry",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/utils": "1.7.0-beta.9",
"@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-alpha.4", "version": "1.7.0-beta.9",
"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-alpha.4", "@nocobase/server": "1.7.0-beta.9",
"@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-alpha.4", "version": "1.7.0-beta.9",
"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

@ -37,7 +37,7 @@ vi.mock('@formily/json-schema', () => {
const result = { ...schema, parent }; const result = { ...schema, parent };
result._isJSONSchemaObject = true; result._isJSONSchemaObject = true;
return result; return result;
}) }),
}; };
}); });
@ -261,26 +261,26 @@ describe('transformMultiColumnToSingleColumn', () => {
name: 'grid1', name: 'grid1',
'x-component': 'Grid', 'x-component': 'Grid',
properties: { properties: {
row1: { row1: {
'x-component': 'Grid.Row', 'x-component': 'Grid.Row',
properties: { properties: {
col1: { 'x-component': 'Input' }, col1: { 'x-component': 'Input' },
col2: { 'x-component': 'Select' }, col2: { 'x-component': 'Select' },
},
}, },
}, },
},
parent: { parent: {
properties: { properties: {
grid1: {} // Will be replaced by result grid1: {}, // Will be replaced by result
} },
}, },
toJSON: vi.fn().mockImplementation(function() { toJSON: vi.fn().mockImplementation(function () {
return { return {
name: this.name, name: this.name,
'x-component': this['x-component'], 'x-component': this['x-component'],
properties: this.properties properties: this.properties,
}; };
}) }),
}; };
const result = transformMultiColumnToSingleColumn(mockSchema); const result = transformMultiColumnToSingleColumn(mockSchema);

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-alpha.4", "version": "1.7.0-beta.9",
"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

@ -276,41 +276,43 @@ export const MenuPermissions: React.FC<{
expandable={{ expandable={{
defaultExpandAllRows: false, defaultExpandAllRows: false,
}} }}
columns={[ columns={
{ [
dataIndex: 'title', {
title: t('Route name'), dataIndex: 'title',
}, title: t('Route name'),
{
dataIndex: 'accessible',
title: (
<>
<Checkbox
checked={allChecked}
onChange={async (value) => {
if (allChecked) {
await resource.set({
values: [],
});
} else {
await resource.set({
values: allIDList,
});
}
refresh();
refreshDesktopRoutes();
message.success(t('Saved successfully'));
}}
/>{' '}
{t('Accessible')}
</>
),
render: (_, schema) => {
const checked = IDList.includes(schema.id);
return <Checkbox checked={checked} onChange={() => handleChange(checked, schema)} />;
}, },
}, {
] as TableProps['columns']} dataIndex: 'accessible',
title: (
<>
<Checkbox
checked={allChecked}
onChange={async (value) => {
if (allChecked) {
await resource.set({
values: [],
});
} else {
await resource.set({
values: allIDList,
});
}
refresh();
refreshDesktopRoutes();
message.success(t('Saved successfully'));
}}
/>{' '}
{t('Accessible')}
</>
),
render: (_, schema) => {
const checked = IDList.includes(schema.id);
return <Checkbox checked={checked} onChange={() => handleChange(checked, schema)} />;
},
},
] as TableProps['columns']
}
dataSource={translateTitle(items, t, compile)} dataSource={translateTitle(items, t, compile)}
/> />
</> </>

View File

@ -113,44 +113,46 @@ export const PluginPermissions: React.FC<{
expandable={{ expandable={{
defaultExpandAllRows: true, defaultExpandAllRows: true,
}} }}
columns={[ columns={
{ [
dataIndex: 'title', {
title: t('Plugin name'), dataIndex: 'title',
render: (value) => { title: t('Plugin name'),
return compile(value); render: (value) => {
return compile(value);
},
}, },
}, {
{ dataIndex: 'accessible',
dataIndex: 'accessible', title: (
title: ( <>
<> <Checkbox
<Checkbox checked={allChecked}
checked={allChecked} onChange={async () => {
onChange={async () => { const values = allAclSnippets.map((v) => '!' + v);
const values = allAclSnippets.map((v) => '!' + v); if (!allChecked) {
if (!allChecked) { await resource.remove({
await resource.remove({ values,
values, });
}); } else {
} else { await resource.add({
await resource.add({ values,
values, });
}); }
} refresh();
refresh(); message.success(t('Saved successfully'));
message.success(t('Saved successfully')); }}
}} />
/> {t('Accessible')}
{t('Accessible')} </>
</> ),
), render: (_, record) => {
render: (_, record) => { const checked = !snippets.includes('!' + record.aclSnippet);
const checked = !snippets.includes('!' + record.aclSnippet); return <Checkbox checked={checked} onChange={() => handleChange(checked, record)} />;
return <Checkbox checked={checked} onChange={() => handleChange(checked, record)} />; },
}, },
}, ] as TableProps['columns']
] as TableProps['columns']} }
dataSource={settings dataSource={settings
.filter((v) => { .filter((v) => {
return v.isTopLevel !== false; return v.isTopLevel !== false;

View File

@ -106,48 +106,50 @@ export const RolesResourcesActions = connect((props) => {
className={styles} className={styles}
size={'small'} size={'small'}
pagination={false} pagination={false}
columns={[ columns={
{ [
dataIndex: 'displayName', {
title: t('Action display name'), dataIndex: 'displayName',
render: (value) => compile(value), title: t('Action display name'),
}, render: (value) => compile(value),
{ },
dataIndex: 'onNewRecord', {
title: t('Action type'), dataIndex: 'onNewRecord',
render: (onNewRecord) => title: t('Action type'),
onNewRecord ? ( render: (onNewRecord) =>
<Tag color={'green'}>{t('Action on new records')}</Tag> onNewRecord ? (
) : ( <Tag color={'green'}>{t('Action on new records')}</Tag>
<Tag color={'geekblue'}>{t('Action on existing records')}</Tag> ) : (
), <Tag color={'geekblue'}>{t('Action on existing records')}</Tag>
}, ),
{ },
dataIndex: 'enabled', {
title: t('Allow'), dataIndex: 'enabled',
render: (enabled, action) => ( title: t('Allow'),
<Checkbox render: (enabled, action) => (
checked={enabled} <Checkbox
onChange={() => { checked={enabled}
toggleAction(action.name); onChange={() => {
}} toggleAction(action.name);
/>
),
},
{
dataIndex: 'scope',
title: t('Data scope'),
render: (value, action) =>
!action.onNewRecord && (
<ScopeSelect
value={value}
onChange={(scope) => {
setScope(action.name, scope);
}} }}
/> />
), ),
}, },
] as TableProps['columns']} {
dataIndex: 'scope',
title: t('Data scope'),
render: (value, action) =>
!action.onNewRecord && (
<ScopeSelect
value={value}
onChange={(scope) => {
setScope(action.name, scope);
}}
/>
),
},
] as TableProps['columns']
}
dataSource={availableActions?.map((item) => { dataSource={availableActions?.map((item) => {
let enabled = false; let enabled = false;
let scope = null; let scope = null;
@ -170,60 +172,62 @@ export const RolesResourcesActions = connect((props) => {
className={styles} className={styles}
pagination={false} pagination={false}
dataSource={fieldPermissions} dataSource={fieldPermissions}
columns={[ columns={
{ [
dataIndex: ['uiSchema', 'title'], {
title: t('Field display name'), dataIndex: ['uiSchema', 'title'],
render: (value) => compile(value), title: t('Field display name'),
}, render: (value) => compile(value),
...availableActionsWithFields.map((action) => { },
const checked = allChecked?.[action.name]; ...availableActionsWithFields.map((action) => {
return { const checked = allChecked?.[action.name];
dataIndex: action.name, return {
title: ( dataIndex: action.name,
<> title: (
<>
<Checkbox
checked={checked}
onChange={() => {
const item = actionMap[action.name] || {
name: action.name,
};
if (checked) {
item.fields = [];
} else {
item.fields = collectionFields?.map?.((item) => item.name);
}
actionMap[action.name] = item;
onChange(Object.values(actionMap));
}}
/>{' '}
{compile(action.displayName)}
</>
),
render: (checked, field) => (
<Checkbox <Checkbox
checked={checked} checked={checked}
aria-label={`${action.name}_checkbox`}
onChange={() => { onChange={() => {
const item = actionMap[action.name] || { const item = actionMap[action.name] || {
name: action.name, name: action.name,
}; };
const fields: string[] = item.fields || [];
if (checked) { if (checked) {
item.fields = []; const index = fields.indexOf(field.name);
fields.splice(index, 1);
} else { } else {
item.fields = collectionFields?.map?.((item) => item.name); fields.push(field.name);
} }
item.fields = fields;
actionMap[action.name] = item; actionMap[action.name] = item;
onChange(Object.values(actionMap)); onChange(Object.values(actionMap));
}} }}
/>{' '} />
{compile(action.displayName)} ),
</> };
), }),
render: (checked, field) => ( ] as TableProps['columns']
<Checkbox }
checked={checked}
aria-label={`${action.name}_checkbox`}
onChange={() => {
const item = actionMap[action.name] || {
name: action.name,
};
const fields: string[] = item.fields || [];
if (checked) {
const index = fields.indexOf(field.name);
fields.splice(index, 1);
} else {
fields.push(field.name);
}
item.fields = fields;
actionMap[action.name] = item;
onChange(Object.values(actionMap));
}}
/>
),
};
}),
] as TableProps['columns']}
/> />
</FormItem> </FormItem>
</FormLayout> </FormLayout>

View File

@ -55,62 +55,64 @@ export const StrategyActions = connect((props) => {
rowKey={'name'} rowKey={'name'}
size={'small'} size={'small'}
pagination={false} pagination={false}
columns={[ columns={
{ [
dataIndex: 'displayName', {
title: t('Action display name'), dataIndex: 'displayName',
render: (value) => compile(value), title: t('Action display name'),
}, render: (value) => compile(value),
{ },
dataIndex: 'onNewRecord', {
title: t('Action type'), dataIndex: 'onNewRecord',
render: (onNewRecord) => title: t('Action type'),
onNewRecord ? ( render: (onNewRecord) =>
<Tag color={'green'}>{t('Action on new records')}</Tag> onNewRecord ? (
) : ( <Tag color={'green'}>{t('Action on new records')}</Tag>
<Tag color={'geekblue'}>{t('Action on existing records')}</Tag> ) : (
), <Tag color={'geekblue'}>{t('Action on existing records')}</Tag>
}, ),
{ },
dataIndex: 'enabled', {
title: t('Allow'), dataIndex: 'enabled',
render: (enabled, action) => ( title: t('Allow'),
<Checkbox render: (enabled, action) => (
checked={enabled} <Checkbox
aria-label={`${action.name}_checkbox`} checked={enabled}
onChange={(e) => { aria-label={`${action.name}_checkbox`}
if (enabled) { onChange={(e) => {
delete scopes[action.name]; if (enabled) {
} else { delete scopes[action.name];
scopes[action.name] = 'all'; } else {
} scopes[action.name] = 'all';
onChange(toFieldValue(scopes)); }
}}
/>
),
},
{
dataIndex: 'scope',
title: t('Data scope'),
render: (scope, action) =>
!action.onNewRecord && (
<Select
data-testid="select-data-scope"
popupMatchSelectWidth={false}
size={'small'}
value={scope}
options={[
{ label: t('All records'), value: 'all' },
{ label: t('Own records'), value: 'own' },
]}
onChange={(value) => {
scopes[action.name] = value;
onChange(toFieldValue(scopes)); onChange(toFieldValue(scopes));
}} }}
/> />
), ),
}, },
] as TableProps['columns']} {
dataIndex: 'scope',
title: t('Data scope'),
render: (scope, action) =>
!action.onNewRecord && (
<Select
data-testid="select-data-scope"
popupMatchSelectWidth={false}
size={'small'}
value={scope}
options={[
{ label: t('All records'), value: 'all' },
{ label: t('Own records'), value: 'own' },
]}
onChange={(value) => {
scopes[action.name] = value;
onChange(toFieldValue(scopes));
}}
/>
),
},
] as TableProps['columns']
}
dataSource={availableActions?.map((item) => { dataSource={availableActions?.map((item) => {
let scope = 'all'; let scope = 'all';
let enabled = false; let enabled = false;

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-action-bulk-edit", "name": "@nocobase/plugin-action-bulk-edit",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"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

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-action-bulk-update", "name": "@nocobase/plugin-action-bulk-update",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"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",

View File

@ -8,7 +8,6 @@
*/ */
import { ISchema, useFieldSchema } from '@formily/react'; import { ISchema, useFieldSchema } from '@formily/react';
import { isValid } from '@formily/shared';
import { import {
ActionDesigner, ActionDesigner,
SchemaSettings, SchemaSettings,
@ -19,6 +18,8 @@ import {
useDesignable, useDesignable,
useSchemaToolbar, useSchemaToolbar,
RefreshDataBlockRequest, RefreshDataBlockRequest,
useAfterSuccessOptions,
useGlobalVariable,
} from '@nocobase/client'; } from '@nocobase/client';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import React from 'react'; import React from 'react';
@ -49,11 +50,22 @@ function UpdateMode() {
/> />
); );
} }
const fieldNames = {
value: 'value',
label: 'label',
};
const useVariableProps = (environmentVariables) => {
const scope = useAfterSuccessOptions();
return {
scope: [environmentVariables, ...scope].filter(Boolean),
fieldNames,
};
};
function AfterSuccess() { function AfterSuccess() {
const { dn } = useDesignable(); const { dn } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const environmentVariables = useGlobalVariable('$env');
return ( return (
<SchemaSettingsModalItem <SchemaSettingsModalItem
title={t('After successful submission')} title={t('After successful submission')}
@ -100,8 +112,9 @@ function AfterSuccess() {
redirectTo: { redirectTo: {
title: t('Link'), title: t('Link'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input', 'x-component': 'Variable.TextArea',
'x-component-props': {}, // eslint-disable-next-line react-hooks/rules-of-hooks
'x-use-component-props': () => useVariableProps(environmentVariables),
}, },
}, },
} as ISchema } as ISchema

View File

@ -68,7 +68,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
if (result) { if (result) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
} }
}); });

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-action-custom-request", "name": "@nocobase/plugin-action-custom-request",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-custom-request", "homepage": "https://docs.nocobase.com/handbook/action-custom-request",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-custom-request", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-custom-request",

View File

@ -68,10 +68,13 @@ export const useCustomizeRequestActionProps = () => {
responseType: fieldSchema['x-response-type'] === 'stream' ? 'blob' : 'json', responseType: fieldSchema['x-response-type'] === 'stream' ? 'blob' : 'json',
}); });
if (res.headers['content-disposition']) { if (res.headers['content-disposition']) {
const regex = /attachment;\s*filename="([^"]+)"/; const contentDisposition = res.headers['content-disposition'];
const match = res.headers['content-disposition'].match(regex); const utf8Match = contentDisposition.match(/filename\*=utf-8''([^;]+)/i);
if (match?.[1]) { const asciiMatch = contentDisposition.match(/filename="([^"]+)"/i);
saveAs(res.data, match[1]); if (utf8Match) {
saveAs(res.data, decodeURIComponent(utf8Match[1]));
} else if (asciiMatch) {
saveAs(res.data, asciiMatch[1]);
} }
} }
actionField.data.loading = false; actionField.data.loading = false;

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-action-duplicate", "name": "@nocobase/plugin-action-duplicate",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-duplicate", "homepage": "https://docs.nocobase.com/handbook/action-duplicate",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-duplicate", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-duplicate",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "操作:导出记录", "displayName.zh-CN": "操作:导出记录",
"description": "Export filtered records to excel, you can configure which fields to export.", "description": "Export filtered records to excel, you can configure which fields to export.",
"description.zh-CN": "导出筛选后的记录到 Excel 中,可以配置导出哪些字段。", "description.zh-CN": "导出筛选后的记录到 Excel 中,可以配置导出哪些字段。",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-export", "homepage": "https://docs.nocobase.com/handbook/action-export",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "操作:导入记录", "displayName.zh-CN": "操作:导入记录",
"description": "Import records using excel templates. You can configure which fields to import and templates will be generated automatically.", "description": "Import records using excel templates. You can configure which fields to import and templates will be generated automatically.",
"description.zh-CN": "使用 Excel 模板导入数据,可以配置导入哪些字段,自动生成模板。", "description.zh-CN": "使用 Excel 模板导入数据,可以配置导入哪些字段,自动生成模板。",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-import", "homepage": "https://docs.nocobase.com/handbook/action-import",

View File

@ -12,6 +12,7 @@ import { ISchema, useFieldSchema } from '@formily/react';
import { Action, ActionContextProvider, PopupSettingsProvider, SchemaComponent, useCompile } from '@nocobase/client'; import { Action, ActionContextProvider, PopupSettingsProvider, SchemaComponent, useCompile } from '@nocobase/client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { NAMESPACE } from './constants'; import { NAMESPACE } from './constants';
import { useTranslation } from 'react-i18next';
import { UploadOutlined } from '@ant-design/icons'; import { UploadOutlined } from '@ant-design/icons';
const importFormSchema: ISchema = { const importFormSchema: ISchema = {
@ -128,7 +129,7 @@ export const ImportAction = (props) => {
return ( return (
<ActionContextProvider value={{ visible, setVisible, fieldSchema }}> <ActionContextProvider value={{ visible, setVisible, fieldSchema }}>
<Action <Action
icon={props.icon || <UploadOutlined />} icon={props.icon || 'uploadoutlined'}
title={compile(fieldSchema?.title || "t('Import')")} title={compile(fieldSchema?.title || "t('Import')")}
{...props} {...props}
onClick={() => setVisible(true)} onClick={() => setVisible(true)}

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-action-print", "name": "@nocobase/plugin-action-print",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-print", "homepage": "https://docs.nocobase.com/handbook/action-print",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-print", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-print",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "AI 集成", "displayName.zh-CN": "AI 集成",
"description": "Support integration with AI services, providing AI-related workflow nodes to enhance business processing capabilities.", "description": "Support integration with AI services, providing AI-related workflow nodes to enhance business processing capabilities.",
"description.zh-CN": "支持接入 AI 服务,提供 AI 相关的工作流节点,增强业务处理能力。", "description.zh-CN": "支持接入 AI 服务,提供 AI 相关的工作流节点,增强业务处理能力。",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"peerDependencies": { "peerDependencies": {
"@nocobase/client": "1.x", "@nocobase/client": "1.x",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-api-doc", "name": "@nocobase/plugin-api-doc",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"displayName": "API documentation", "displayName": "API documentation",
"displayName.zh-CN": "API 文档", "displayName.zh-CN": "API 文档",
"description": "An OpenAPI documentation generator for NocoBase HTTP API.", "description": "An OpenAPI documentation generator for NocoBase HTTP API.",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "认证API 密钥", "displayName.zh-CN": "认证API 密钥",
"description": "Allows users to use API key to access application's HTTP API", "description": "Allows users to use API key to access application's HTTP API",
"description.zh-CN": "允许用户使用 API 密钥访问应用的 HTTP API", "description.zh-CN": "允许用户使用 API 密钥访问应用的 HTTP API",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/api-keys", "homepage": "https://docs.nocobase.com/handbook/api-keys",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "异步任务管理器", "displayName.zh-CN": "异步任务管理器",
"description": "Manage and monitor asynchronous tasks such as data import/export. Support task progress tracking and notification.", "description": "Manage and monitor asynchronous tasks such as data import/export. Support task progress tracking and notification.",
"description.zh-CN": "管理和监控数据导入导出等异步任务。支持任务进度跟踪和通知。", "description.zh-CN": "管理和监控数据导入导出等异步任务。支持任务进度跟踪和通知。",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"peerDependencies": { "peerDependencies": {
"@nocobase/client": "1.x", "@nocobase/client": "1.x",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-audit-logs", "name": "@nocobase/plugin-audit-logs",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"displayName": "Audit logs (deprecated)", "displayName": "Audit logs (deprecated)",
"displayName.zh-CN": "审计日志(废弃)", "displayName.zh-CN": "审计日志(废弃)",
"description": "This plugin is deprecated. There will be a new audit log plugin in the future.", "description": "This plugin is deprecated. There will be a new audit log plugin in the future.",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "认证:短信", "displayName.zh-CN": "认证:短信",
"description": "SMS authentication.", "description": "SMS authentication.",
"description.zh-CN": "通过短信验证码认证身份。", "description.zh-CN": "通过短信验证码认证身份。",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/auth-sms", "homepage": "https://docs.nocobase.com/handbook/auth-sms",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/auth-sms", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/auth-sms",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-auth", "name": "@nocobase/plugin-auth",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/auth", "homepage": "https://docs.nocobase.com/handbook/auth",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/auth", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/auth",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "应用的备份与还原(废弃)", "displayName.zh-CN": "应用的备份与还原(废弃)",
"description": "Backup and restore applications for scenarios such as application replication, migration, and upgrades.", "description": "Backup and restore applications for scenarios such as application replication, migration, and upgrades.",
"description.zh-CN": "备份和还原应用,可用于应用的复制、迁移、升级等场景。", "description.zh-CN": "备份和还原应用,可用于应用的复制、迁移、升级等场景。",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/backup-restore", "homepage": "https://docs.nocobase.com/handbook/backup-restore",

View File

@ -111,11 +111,11 @@ const LearnMore: any = (props: { collectionsData?: any; isBackup?: boolean }) =>
render: (_, data) => { render: (_, data) => {
const title = compile(data.title); const title = compile(data.title);
const name = data.name; const name = data.name;
return name === title ? title : ( return name === title ? (
title
) : (
<div> <div>
{data.name} {data.name} <span style={{ color: 'rgba(0, 0, 0, 0.3)', fontSize: '0.9em' }}>({compile(data.title)})</span>
{' '}
<span style={{ color: 'rgba(0, 0, 0, 0.3)', fontSize: '0.9em' }}>({compile(data.title)})</span>
</div> </div>
); );
}, },
@ -418,73 +418,75 @@ export const BackupAndRestoreList = () => {
<Table <Table
dataSource={dataSource} dataSource={dataSource}
loading={loading} loading={loading}
columns={[ columns={
{ [
title: t('Backup file'), {
dataIndex: 'name', title: t('Backup file'),
width: 400, dataIndex: 'name',
onCell: (data) => { width: 400,
return data.inProgress onCell: (data) => {
? { return data.inProgress
colSpan: 4, ? {
} colSpan: 4,
: {}; }
: {};
},
render: (name, data) =>
data.inProgress ? (
<div style={{ color: 'rgba(0, 0, 0, 0.88)' }}>
{name}({t('Backing up')}...)
</div>
) : (
<div>{name}</div>
),
}, },
render: (name, data) => {
data.inProgress ? ( title: t('File size'),
<div style={{ color: 'rgba(0, 0, 0, 0.88)' }}> dataIndex: 'fileSize',
{name}({t('Backing up')}...) onCell: (data) => {
</div> return data.inProgress
) : ( ? {
<div>{name}</div> colSpan: 0,
}
: {};
},
},
{
title: t('Created at', { ns: 'client' }),
dataIndex: 'createdAt',
onCell: (data) => {
return data.inProgress
? {
colSpan: 0,
}
: {};
},
render: (value) => {
return <DatePicker.ReadPretty value={value} showTime />;
},
},
{
title: t('Actions', { ns: 'client' }),
dataIndex: 'actions',
onCell: (data) => {
return data.inProgress
? {
colSpan: 0,
}
: {};
},
render: (_, record) => (
<Space split={<Divider type="vertical" />}>
<Restore ButtonComponent={'a'} title={t('Restore')} fileData={record} />
<a type="link" onClick={() => handleDownload(record)}>
{t('Download')}
</a>
<a onClick={() => handleDestory(record)}>{t('Delete')}</a>
</Space>
), ),
},
{
title: t('File size'),
dataIndex: 'fileSize',
onCell: (data) => {
return data.inProgress
? {
colSpan: 0,
}
: {};
}, },
}, ] as TableProps['columns']
{ }
title: t('Created at', { ns: 'client' }),
dataIndex: 'createdAt',
onCell: (data) => {
return data.inProgress
? {
colSpan: 0,
}
: {};
},
render: (value) => {
return <DatePicker.ReadPretty value={value} showTime />;
},
},
{
title: t('Actions', { ns: 'client' }),
dataIndex: 'actions',
onCell: (data) => {
return data.inProgress
? {
colSpan: 0,
}
: {};
},
render: (_, record) => (
<Space split={<Divider type="vertical" />}>
<Restore ButtonComponent={'a'} title={t('Restore')} fileData={record} />
<a type="link" onClick={() => handleDownload(record)}>
{t('Download')}
</a>
<a onClick={() => handleDestory(record)}>{t('Delete')}</a>
</Space>
),
},
] as TableProps['columns']}
/> />
</Card> </Card>
</div> </div>

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "区块iframe", "displayName.zh-CN": "区块iframe",
"description": "Create an iframe block on the page to embed and display external web pages or content.", "description": "Create an iframe block on the page to embed and display external web pages or content.",
"description.zh-CN": "在页面上创建和管理iframe用于嵌入和展示外部网页或内容。", "description.zh-CN": "在页面上创建和管理iframe用于嵌入和展示外部网页或内容。",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/block-iframe", "homepage": "https://docs.nocobase.com/handbook/block-iframe",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "区块:模板", "displayName.zh-CN": "区块:模板",
"description": "Create and manage block templates for reuse on pages.", "description": "Create and manage block templates for reuse on pages.",
"description.zh-CN": "创建和管理区块模板,用于在页面中重复使用。", "description.zh-CN": "创建和管理区块模板,用于在页面中重复使用。",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/block-template", "homepage": "https://docs.nocobase.com/handbook/block-template",

View File

@ -23,7 +23,7 @@ import { useLocation } from 'react-router-dom';
const blockDecoratorMenuMaps = { const blockDecoratorMenuMaps = {
TableBlockProvider: ['Table', 'table'], TableBlockProvider: ['Table', 'table'],
FormBlockProvider: ['Form', 'form'], FormBlockProvider: ['FormItem', 'form'],
DetailsBlockProvider: ['Details', 'details'], DetailsBlockProvider: ['Details', 'details'],
'List.Decorator': ['List', 'list'], 'List.Decorator': ['List', 'list'],
'GridCard.Decorator': ['GridCard', 'gridCard'], 'GridCard.Decorator': ['GridCard', 'gridCard'],
@ -281,6 +281,7 @@ function getTemplateSchemaFromPage(schema: ISchema) {
if (s['x-template-root-uid']) { if (s['x-template-root-uid']) {
return; return;
} }
t = t || {};
_.merge(t, _.omit(s, ['x-uid', 'properties'])); _.merge(t, _.omit(s, ['x-uid', 'properties']));
t['x-uid'] = uid(); t['x-uid'] = uid();
if (s.properties) { if (s.properties) {
@ -288,7 +289,7 @@ function getTemplateSchemaFromPage(schema: ISchema) {
if (s.properties[key]['x-template-root-uid']) { if (s.properties[key]['x-template-root-uid']) {
continue; continue;
} }
_.set(t, `properties.${key}`, {}); _.set(t, `properties.['${key}']`, {});
traverseSchema(s.properties[key], t.properties[key]); traverseSchema(s.properties[key], t.properties[key]);
} }
} }

View File

@ -169,6 +169,9 @@ export class PluginBlockTemplateClient extends Plugin {
'blockSettings:createForm', 'blockSettings:createForm',
'blockSettings:details', 'blockSettings:details',
'blockSettings:detailsWithPagination', 'blockSettings:detailsWithPagination',
'blockSettings:multiDataDetails',
'blockSettings:singleDataDetails',
'blockSettings:stepsForm',
'blockSettings:filterCollapse', 'blockSettings:filterCollapse',
'blockSettings:filterForm', 'blockSettings:filterForm',
'blockSettings:gantt', 'blockSettings:gantt',
@ -176,6 +179,13 @@ export class PluginBlockTemplateClient extends Plugin {
'blockSettings:kanban', 'blockSettings:kanban',
'blockSettings:list', 'blockSettings:list',
'blockSettings:table', 'blockSettings:table',
'blockSettings:tree',
'ReadPrettyFormSettings',
'GanttBlockSettings',
'FormV1Settings',
'FormSettings',
'FormItemSettings',
'FormDetailsSettings',
]; ];
if (blockSettings.includes(key)) { if (blockSettings.includes(key)) {
// schemaSetting.add('template-saveAsTemplateItem', saveAsTemplateSetting); // schemaSetting.add('template-saveAsTemplateItem', saveAsTemplateSetting);

View File

@ -43,6 +43,10 @@ export function convertTplBlock(
if (newSchema['x-decorator'] === 'TemplateGridDecorator') { if (newSchema['x-decorator'] === 'TemplateGridDecorator') {
delete newSchema['x-decorator']; delete newSchema['x-decorator'];
} }
if (newSchema['x-linkage-rules']) {
// linkage rules 有可能保存在Grid组件中
delete newSchema['x-linkage-rules'];
}
for (const key in tpl.properties) { for (const key in tpl.properties) {
const t = convertTplBlock(tpl.properties[key], virtual, isRoot, newRootId, templateKey, options); const t = convertTplBlock(tpl.properties[key], virtual, isRoot, newRootId, templateKey, options);
if (isRoot) { if (isRoot) {
@ -154,8 +158,12 @@ export function formSchemaPatch(currentSchema: ISchema, options?: any) {
return key !== 'grid'; return key !== 'grid';
}); });
if (actionKey) { if (actionKey) {
_.set(currentSchema, `properties.${comKey}.x-use-component-props`, 'useEditFormBlockProps'); _.set(currentSchema, `properties.['${comKey}'].x-use-component-props`, 'useEditFormBlockProps');
_.set(currentSchema, `properties.${comKey}.properties.${actionKey}.x-initializer`, 'editForm:configureActions'); _.set(
currentSchema,
`properties.['${comKey}'].properties.['${actionKey}'].x-initializer`,
'editForm:configureActions',
);
const actionBarSchema = _.get(currentSchema, `properties.${comKey}.properties.${actionKey}.properties`, {}); const actionBarSchema = _.get(currentSchema, `properties.${comKey}.properties.${actionKey}.properties`, {});
for (const key in actionBarSchema) { for (const key in actionBarSchema) {

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-block-workbench", "name": "@nocobase/plugin-block-workbench",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"displayName": "Block: Action panel", "displayName": "Block: Action panel",
"displayName.zh-CN": "区块:操作面板", "displayName.zh-CN": "区块:操作面板",
"description": "Centrally manages and displays various actions, allowing users to efficiently perform tasks. It supports extensibility, with current action types including pop-ups, links, scanning, and custom requests.", "description": "Centrally manages and displays various actions, allowing users to efficiently perform tasks. It supports extensibility, with current action types including pop-ups, links, scanning, and custom requests.",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-calendar", "name": "@nocobase/plugin-calendar",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"displayName": "Calendar", "displayName": "Calendar",
"displayName.zh-CN": "日历", "displayName.zh-CN": "日历",
"description": "Provides callendar collection template and block for managing date data, typically for date/time related information such as events, appointments, tasks, and so on.", "description": "Provides callendar collection template and block for managing date data, typically for date/time related information such as events, appointments, tasks, and so on.",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "图表(废弃)", "displayName.zh-CN": "图表(废弃)",
"description": "The plugin has been deprecated, please use the data visualization plugin instead.", "description": "The plugin has been deprecated, please use the data visualization plugin instead.",
"description.zh-CN": "已废弃插件,请使用数据可视化插件代替。", "description.zh-CN": "已废弃插件,请使用数据可视化插件代替。",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "WEB 客户端", "displayName.zh-CN": "WEB 客户端",
"description": "Provides a client interface for the NocoBase server", "description": "Provides a client interface for the NocoBase server",
"description.zh-CN": "为 NocoBase 服务端提供客户端界面", "description.zh-CN": "为 NocoBase 服务端提供客户端界面",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "数据表: SQL", "displayName.zh-CN": "数据表: SQL",
"description": "Provides SQL collection template", "description": "Provides SQL collection template",
"description.zh-CN": "提供 SQL 数据表模板", "description.zh-CN": "提供 SQL 数据表模板",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"homepage": "https://docs-cn.nocobase.com/handbook/collection-sql", "homepage": "https://docs-cn.nocobase.com/handbook/collection-sql",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/collection-sql", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/collection-sql",
"main": "dist/server/index.js", "main": "dist/server/index.js",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-collection-tree", "name": "@nocobase/plugin-collection-tree",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"displayName": "Collection: Tree", "displayName": "Collection: Tree",
"displayName.zh-CN": "数据表:树", "displayName.zh-CN": "数据表:树",
"description": "Provides tree collection template", "description": "Provides tree collection template",

View File

@ -31,6 +31,8 @@ export default class extends Migration {
await this.db.sequelize.transaction(async (transaction) => { await this.db.sequelize.transaction(async (transaction) => {
const treeCollections = await this.getTreeCollections({ transaction }); const treeCollections = await this.getTreeCollections({ transaction });
for (const treeCollection of treeCollections) { for (const treeCollection of treeCollections) {
await treeCollection.load({ transaction });
const name = `main_${treeCollection.name}_path`; const name = `main_${treeCollection.name}_path`;
const collectionOptions = { const collectionOptions = {
name, name,
@ -47,35 +49,16 @@ export default class extends Migration {
}, },
], ],
}; };
const collectionInstance = this.db.getCollection(treeCollection.name); const collectionInstance = this.db.getCollection(treeCollection.name);
const treeCollectionSchema = collectionInstance.collectionSchema(); const treeCollectionSchema = collectionInstance.collectionSchema();
if (this.app.db.inDialect('postgres') && treeCollectionSchema != this.app.db.options.schema) { if (this.app.db.inDialect('postgres') && treeCollectionSchema != this.app.db.options.schema) {
collectionOptions['schema'] = treeCollectionSchema; collectionOptions['schema'] = treeCollectionSchema;
} }
this.app.db.collection(collectionOptions); this.app.db.collection(collectionOptions);
const treeExistsInDb = await this.app.db.getCollection(name).existsInDb({ transaction }); const treeExistsInDb = await this.app.db.getCollection(name).existsInDb({ transaction });
if (!treeExistsInDb) { if (!treeExistsInDb) {
await this.app.db.getCollection(name).sync({ transaction } as SyncOptions); await this.app.db.getCollection(name).sync({ transaction } as SyncOptions);
const opts = {
name: treeCollection.name,
autoGenId: false,
timestamps: false,
fields: [
{ type: 'integer', name: 'id' },
{ type: 'integer', name: 'parentId' },
],
};
if (treeCollectionSchema != this.app.db.options.schema) {
opts['schema'] = treeCollectionSchema;
}
this.app.db.collection(opts);
const chunkSize = 1000; const chunkSize = 1000;
await this.app.db.getRepository(treeCollection.name).chunk({ await this.app.db.getRepository(treeCollection.name).chunk({
chunkSize: chunkSize, chunkSize: chunkSize,

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "数据源:主数据库", "displayName.zh-CN": "数据源:主数据库",
"description": "NocoBase main database, supports relational databases such as PostgreSQL, MySQL, MariaDB and so on.", "description": "NocoBase main database, supports relational databases such as PostgreSQL, MySQL, MariaDB and so on.",
"description.zh-CN": "NocoBase 主数据库,支持 PostgreSQL、MySQL、MariaDB 等关系型数据库。", "description.zh-CN": "NocoBase 主数据库,支持 PostgreSQL、MySQL、MariaDB 等关系型数据库。",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/data-source-main", "homepage": "https://docs.nocobase.com/handbook/data-source-main",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/data-source-main", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/data-source-main",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-data-source-manager", "name": "@nocobase/plugin-data-source-manager",
"version": "1.7.0-alpha.4", "version": "1.7.0-beta.9",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"displayName": "Data source manager", "displayName": "Data source manager",
"displayName.zh-CN": "数据源管理", "displayName.zh-CN": "数据源管理",

View File

@ -101,48 +101,50 @@ export const RolesResourcesActions = connect((props) => {
className={styles} className={styles}
size={'small'} size={'small'}
pagination={false} pagination={false}
columns={[ columns={
{ [
dataIndex: 'displayName', {
title: t('Action display name'), dataIndex: 'displayName',
render: (value) => compile(value), title: t('Action display name'),
}, render: (value) => compile(value),
{ },
dataIndex: 'onNewRecord', {
title: t('Action type'), dataIndex: 'onNewRecord',
render: (onNewRecord) => title: t('Action type'),
onNewRecord ? ( render: (onNewRecord) =>
<Tag color={'green'}>{t('Action on new records')}</Tag> onNewRecord ? (
) : ( <Tag color={'green'}>{t('Action on new records')}</Tag>
<Tag color={'geekblue'}>{t('Action on existing records')}</Tag> ) : (
), <Tag color={'geekblue'}>{t('Action on existing records')}</Tag>
}, ),
{ },
dataIndex: 'enabled', {
title: t('Allow'), dataIndex: 'enabled',
render: (enabled, action) => ( title: t('Allow'),
<Checkbox render: (enabled, action) => (
checked={enabled} <Checkbox
onChange={() => { checked={enabled}
toggleAction(action.name); onChange={() => {
}} toggleAction(action.name);
/>
),
},
{
dataIndex: 'scope',
title: t('Data scope'),
render: (value, action) =>
!action.onNewRecord && (
<ScopeSelect
value={value}
onChange={(scope) => {
setScope(action.name, scope);
}} }}
/> />
), ),
}, },
] as TableProps['columns']} {
dataIndex: 'scope',
title: t('Data scope'),
render: (value, action) =>
!action.onNewRecord && (
<ScopeSelect
value={value}
onChange={(scope) => {
setScope(action.name, scope);
}}
/>
),
},
] as TableProps['columns']
}
dataSource={availableActions?.map((item) => { dataSource={availableActions?.map((item) => {
let enabled = false; let enabled = false;
let scope = null; let scope = null;
@ -165,59 +167,61 @@ export const RolesResourcesActions = connect((props) => {
className={styles} className={styles}
pagination={false} pagination={false}
dataSource={fieldPermissions} dataSource={fieldPermissions}
columns={[ columns={
{ [
dataIndex: ['uiSchema', 'title'], {
title: t('Field display name'), dataIndex: ['uiSchema', 'title'],
render: (value, record) => compile(value) || record.name, title: t('Field display name'),
}, render: (value, record) => compile(value) || record.name,
...availableActionsWithFields.map((action) => { },
const checked = allChecked?.[action.name]; ...availableActionsWithFields.map((action) => {
return { const checked = allChecked?.[action.name];
dataIndex: action.name, return {
title: ( dataIndex: action.name,
<> title: (
<>
<Checkbox
checked={checked}
onChange={() => {
const item = actionMap[action.name] || {
name: action.name,
};
if (checked) {
item.fields = [];
} else {
item.fields = collectionFields?.map?.((item) => item.name);
}
actionMap[action.name] = item;
onChange(Object.values(actionMap));
}}
/>
{compile(action.displayName)}
</>
),
render: (checked, field) => (
<Checkbox <Checkbox
checked={checked} checked={checked}
onChange={() => { onChange={() => {
const item = actionMap[action.name] || { const item = actionMap[action.name] || {
name: action.name, name: action.name,
}; };
const fields: string[] = item.fields || [];
if (checked) { if (checked) {
item.fields = []; const index = fields.indexOf(field.name);
fields.splice(index, 1);
} else { } else {
item.fields = collectionFields?.map?.((item) => item.name); fields.push(field.name);
} }
item.fields = fields;
actionMap[action.name] = item; actionMap[action.name] = item;
onChange(Object.values(actionMap)); onChange(Object.values(actionMap));
}} }}
/> />
{compile(action.displayName)} ),
</> };
), }),
render: (checked, field) => ( ] as TableProps['columns']
<Checkbox }
checked={checked}
onChange={() => {
const item = actionMap[action.name] || {
name: action.name,
};
const fields: string[] = item.fields || [];
if (checked) {
const index = fields.indexOf(field.name);
fields.splice(index, 1);
} else {
fields.push(field.name);
}
item.fields = fields;
actionMap[action.name] = item;
onChange(Object.values(actionMap));
}}
/>
),
};
}),
] as TableProps['columns']}
/> />
</FormItem> </FormItem>
</FormLayout> </FormLayout>

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