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/)
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
### 🐛 Bug Fixes

View File

@ -5,6 +5,25 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](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
### 🐛 修复

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/build",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"description": "Library build tool based on rollup.",
"main": "lib/index.js",
"types": "./lib/index.d.ts",

View File

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

View File

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

View File

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

View File

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

View File

@ -121,40 +121,42 @@ export const MenuConfigure = () => {
expandable={{
defaultExpandAllRows: true,
}}
columns={[
{
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)} />;
columns={
[
{
dataIndex: 'title',
title: t('Menu item title'),
},
},
] 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)}
/>
);

View File

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

View File

@ -55,62 +55,64 @@ export const StrategyActions = connect((props) => {
size={'small'}
pagination={false}
rowKey={'name'}
columns={[
{
dataIndex: 'displayName',
title: t('Action display name'),
render: (value) => compile(value),
},
{
dataIndex: 'onNewRecord',
title: t('Action type'),
render: (onNewRecord) =>
onNewRecord ? (
<Tag color={'green'}>{t('Action on new records')}</Tag>
) : (
<Tag color={'geekblue'}>{t('Action on existing records')}</Tag>
),
},
{
dataIndex: 'enabled',
title: t('Allow'),
render: (enabled, action) => (
<Checkbox
checked={enabled}
aria-label={`${action.name}_checkbox`}
onChange={(e) => {
if (enabled) {
delete scopes[action.name];
} else {
scopes[action.name] = 'all';
}
onChange(toFieldValue(scopes));
}}
/>
),
},
{
dataIndex: 'scope',
title: t('Data scope'),
render: (scope, action) =>
!action.onNewRecord && (
<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;
columns={
[
{
dataIndex: 'displayName',
title: t('Action display name'),
render: (value) => compile(value),
},
{
dataIndex: 'onNewRecord',
title: t('Action type'),
render: (onNewRecord) =>
onNewRecord ? (
<Tag color={'green'}>{t('Action on new records')}</Tag>
) : (
<Tag color={'geekblue'}>{t('Action on existing records')}</Tag>
),
},
{
dataIndex: 'enabled',
title: t('Allow'),
render: (enabled, action) => (
<Checkbox
checked={enabled}
aria-label={`${action.name}_checkbox`}
onChange={(e) => {
if (enabled) {
delete scopes[action.name];
} else {
scopes[action.name] = 'all';
}
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) => {
let scope = 'all';
let enabled = false;

View File

@ -167,7 +167,7 @@ export function useCollectValuesToSubmit() {
if (parsedValue !== null && parsedValue !== undefined) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
} else if (value !== '') {
assignedValues[key] = value;
}
});
@ -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 = () => {
const filterByTk = useFilterByTk();
const record = useCollectionRecord();
@ -219,11 +225,20 @@ export const useCreateActionProps = () => {
const collectValues = useCollectValuesToSubmit();
const action = record.isNew ? actionField.componentProps.saveMode || 'create' : 'update';
const filterKeys = actionField.componentProps.filterKeys?.checked || [];
const localVariables = useLocalVariables();
const variables = useVariables();
return {
async onClick() {
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) {
await form.submit();
}
@ -241,6 +256,15 @@ export const useCreateActionProps = () => {
: undefined,
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)) {
setVisible?.(false);
}
@ -338,7 +362,7 @@ export const useAssociationCreateActionProps = () => {
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
} else if (value !== '') {
assignedValues[key] = value;
}
});
@ -588,7 +612,13 @@ export const useCustomizeUpdateActionProps = () => {
skipValidator,
triggerWorkflows,
} = actionSchema?.['x-action-settings'] ?? {};
const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {};
const {
manualClose,
redirecting,
redirectTo: rawRedirectTo,
successMessage,
actionAfterSuccess,
} = onSuccess || {};
const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key];
@ -605,7 +635,7 @@ export const useCustomizeUpdateActionProps = () => {
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
} else if (value !== '') {
assignedValues[key] = value;
}
});
@ -614,7 +644,7 @@ export const useCustomizeUpdateActionProps = () => {
if (skipValidator === false) {
await form.submit();
}
await resource.update({
const result = await resource.update({
filterByTk,
values: { ...assignedValues },
// 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(',')
: 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)) {
setVisible?.(false);
}
@ -708,7 +748,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
} else if (value !== '') {
assignedValues[key] = value;
}
});
@ -913,7 +953,13 @@ export const useUpdateActionProps = () => {
skipValidator,
triggerWorkflows,
} = actionSchema?.['x-action-settings'] ?? {};
const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {};
const {
manualClose,
redirecting,
redirectTo: rawRedirectTo,
successMessage,
actionAfterSuccess,
} = onSuccess || {};
const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key];
@ -930,7 +976,7 @@ export const useUpdateActionProps = () => {
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
} else if (value !== '') {
assignedValues[key] = value;
}
});
@ -952,7 +998,7 @@ export const useUpdateActionProps = () => {
actionField.data = field.data || {};
actionField.data.loading = true;
try {
await resource.update({
const result = await resource.update({
filterByTk,
values: {
...values,
@ -971,6 +1017,15 @@ export const useUpdateActionProps = () => {
if (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)) {
setVisible?.(false);
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -52,6 +52,7 @@ import { KeepAlive } from './KeepAlive';
import { NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema';
import { MenuSchemaToolbar, ResetThemeTokenAndKeepAlgorithm } from './menuItemSettings';
import { userCenterSettings } from './userCenterSettings';
import { createGlobalStyle, createStyles } from 'antd-style';
export { KeepAlive, NocoBaseDesktopRouteType };
@ -268,7 +269,7 @@ const GroupItem: FC<{ item: any }> = (props) => {
};
const WithTooltip: FC<{ title: string; hidden: boolean }> = (props) => {
const { inHeader } = useContext(headerContext);
const { inHeader } = useContext(HeaderContext);
return (
<RouteContext.Consumer>
@ -406,7 +407,7 @@ const contentStyle = {
paddingInline: 0,
};
const headerContext = React.createContext<{ inHeader: boolean }>({ inHeader: false });
const HeaderContext = React.createContext<{ inHeader: boolean }>({ inHeader: false });
const popoverStyle = css`
.ant-popover-inner {
@ -495,9 +496,38 @@ const collapsedButtonRender = (collapsed, dom) => {
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 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) => {
return <headerContext.Provider value={headerContextValue}>{defaultDom}</headerContext.Provider>;
return <HeaderWrapper>{defaultDom}</HeaderWrapper>;
};
const IsMobileLayoutContext = React.createContext<{
@ -531,6 +561,7 @@ export const InternalAdminLayout = () => {
const doNotChangeCollapsedRef = useRef(false);
const { t } = useMenuTranslation();
const designable = isMobileLayout ? false : _designable;
const { styles } = useHeaderStyle();
const route = useMemo(() => {
return {
@ -544,7 +575,7 @@ export const InternalAdminLayout = () => {
colorBgHeader: token.colorBgHeader,
colorTextMenu: token.colorTextHeaderMenu,
colorTextMenuSelected: token.colorTextHeaderMenuActive,
colorTextMenuActive: token.colorTextHeaderMenuActive,
colorTextMenuActive: token.colorTextHeaderMenuHover,
colorBgMenuItemHover: token.colorBgHeaderMenuHover,
colorBgMenuItemSelected: token.colorBgHeaderMenuActive,
heightLayoutHeader: 46,
@ -591,6 +622,12 @@ export const InternalAdminLayout = () => {
});
}, []);
const menuProps = useMemo(() => {
return {
overflowedIndicatorPopupClassName: styles.headerPopup,
};
}, [styles.headerPopup]);
return (
<DndContext onDragEnd={onDragEnd}>
<ProLayout
@ -612,6 +649,7 @@ export const InternalAdminLayout = () => {
onCollapse={onCollapse}
collapsed={collapsed}
onPageChange={onPageChange}
menuProps={menuProps}
>
<RouteContext.Consumer>
{(value: RouteContextType) => {

View File

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

View File

@ -16,16 +16,19 @@ import Action from './Action';
import { ComposedAction } from './types';
import { Icon } from '../../../icon';
const WrapperComponent = ({ component: Component = 'a', icon, onlyIcon, children, ...restProps }: any) => {
return (
<Component {...restProps}>
<Tooltip title={restProps.title}>
<span style={{ marginRight: 3 }}>{icon && typeof icon === 'string' ? <Icon type={icon} /> : icon}</span>
</Tooltip>
{onlyIcon ? children[1] : children}
</Component>
);
};
const WrapperComponent = React.forwardRef(
({ component: Component = 'a', icon, onlyIcon, children, ...restProps }: any, ref) => {
return (
<Component ref={ref} {...restProps}>
<Tooltip title={restProps.title}>
<span style={{ marginRight: 3 }}>{icon && typeof icon === 'string' ? <Icon type={icon} /> : icon}</span>
</Tooltip>
{onlyIcon ? children[1] : children}
</Component>
);
},
);
WrapperComponent.displayName = 'WrapperComponentLink';
export const ActionLink: ComposedAction = withDynamicSchemaProps(
observer((props: any) => {
@ -34,6 +37,7 @@ export const ActionLink: ComposedAction = withDynamicSchemaProps(
{...props}
component={props.component || WrapperComponent}
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 { run, element, disabled: disableAction } = useAction?.(actionCallback) || ({} as any);
const disabled = form.disabled || field.disabled || field.data?.disabled || propsDisabled || disableAction;
const buttonStyle = useMemo(() => {
return {
...style,
@ -538,6 +537,7 @@ const RenderButtonInner = observer(
Designer: React.ElementType;
designerProps: any;
title: string;
isLink?: boolean;
}) => {
const {
designable,
@ -558,6 +558,7 @@ const RenderButtonInner = observer(
Designer,
designerProps,
title,
isLink,
...others
} = props;
const debouncedClick = useCallback(
@ -583,7 +584,7 @@ const RenderButtonInner = observer(
const actionTitle = title || field?.title;
const { opacity, ...restButtonStyle } = buttonStyle;
const linkStyle = isLink && opacity ? { opacity } : undefined;
return (
<SortableItem
role="button"
@ -592,9 +593,9 @@ const RenderButtonInner = observer(
onMouseEnter={handleMouseEnter}
// @ts-ignore
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}
style={restButtonStyle}
style={isLink ? restButtonStyle : buttonStyle}
onClick={process.env.__E2E__ ? handleButtonClick : debouncedClick} // E2E 中的点击操作都是很快的,如果加上 debounce 会导致 E2E 测试失败
component={tarComponent || Button}
className={classnames(componentCls, hashId, className, 'nb-action')}
@ -602,7 +603,7 @@ const RenderButtonInner = observer(
title={actionTitle}
>
{actionTitle && (
<span className={icon ? 'nb-action-title' : null} style={{ opacity }}>
<span className={icon ? 'nb-action-title' : null} style={linkStyle}>
{actionTitle}
</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/useGetAriaLabelOfModal';
export * from './hooks/useGetAriaLabelOfPopover';
export * from './hooks/useGetAfterSuccessVariablesOptions';
export * from './types';
export * from './zIndexContext';

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ describe('CollectionSelect', () => {
>
<div
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"
>
<div
@ -191,7 +191,7 @@ describe('CollectionSelect', () => {
>
<div
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"
>
<div

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@
import { useField } from '@formily/react';
import { useEffect, useContext } from 'react';
import { LinkageLogicContext } from './context';
import { ActionType } from './type';
const findOption = (dataIndex = [], options) => {
let items = options;
@ -45,9 +46,34 @@ export const useValues = (options) => {
field.data = field.data || {};
const operators = option?.operators;
field.data.operators = operators?.filter((v) => {
if (dataIndex.length > 1) {
return !['value', 'dateScope', 'options'].includes(v.value);
const isOptionField = ['select', 'radioGroup', 'multipleSelect', 'checkboxGroup'].includes(
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;
});
field.data.schema = option?.schema;

View File

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

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",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"main": "src/index.js",
"license": "AGPL-3.0",
"dependencies": {

View File

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

View File

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

View File

@ -1,13 +1,13 @@
{
"name": "@nocobase/devtools",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"description": "",
"license": "AGPL-3.0",
"main": "./src/index.js",
"dependencies": {
"@nocobase/build": "1.7.0-alpha.4",
"@nocobase/client": "1.7.0-alpha.4",
"@nocobase/test": "1.7.0-alpha.4",
"@nocobase/build": "1.7.0-beta.9",
"@nocobase/client": "1.7.0-beta.9",
"@nocobase/test": "1.7.0-beta.9",
"@types/koa": "^2.15.0",
"@types/koa-bodyparser": "^4.3.4",
"@types/lodash": "^4.14.177",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/server",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
@ -10,19 +10,19 @@
"@koa/cors": "^5.0.0",
"@koa/multer": "^3.0.2",
"@koa/router": "^9.4.0",
"@nocobase/acl": "1.7.0-alpha.4",
"@nocobase/actions": "1.7.0-alpha.4",
"@nocobase/auth": "1.7.0-alpha.4",
"@nocobase/cache": "1.7.0-alpha.4",
"@nocobase/data-source-manager": "1.7.0-alpha.4",
"@nocobase/database": "1.7.0-alpha.4",
"@nocobase/evaluators": "1.7.0-alpha.4",
"@nocobase/lock-manager": "1.7.0-alpha.4",
"@nocobase/logger": "1.7.0-alpha.4",
"@nocobase/resourcer": "1.7.0-alpha.4",
"@nocobase/sdk": "1.7.0-alpha.4",
"@nocobase/telemetry": "1.7.0-alpha.4",
"@nocobase/utils": "1.7.0-alpha.4",
"@nocobase/acl": "1.7.0-beta.9",
"@nocobase/actions": "1.7.0-beta.9",
"@nocobase/auth": "1.7.0-beta.9",
"@nocobase/cache": "1.7.0-beta.9",
"@nocobase/data-source-manager": "1.7.0-beta.9",
"@nocobase/database": "1.7.0-beta.9",
"@nocobase/evaluators": "1.7.0-beta.9",
"@nocobase/lock-manager": "1.7.0-beta.9",
"@nocobase/logger": "1.7.0-beta.9",
"@nocobase/resourcer": "1.7.0-beta.9",
"@nocobase/sdk": "1.7.0-beta.9",
"@nocobase/telemetry": "1.7.0-beta.9",
"@nocobase/utils": "1.7.0-beta.9",
"@types/decompress": "4.2.7",
"@types/ini": "^1.3.31",
"@types/koa-send": "^4.1.3",

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/test",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"main": "lib/index.js",
"module": "./src/index.ts",
"types": "./lib/index.d.ts",
@ -51,7 +51,7 @@
},
"dependencies": {
"@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",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.0.0",

View File

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

View File

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

View File

@ -4,7 +4,7 @@
"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.zh-CN": "基于角色、资源和操作的权限控制,可以精确控制界面配置权限、数据操作权限、菜单访问权限、插件权限。",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/acl",

View File

@ -276,41 +276,43 @@ export const MenuPermissions: React.FC<{
expandable={{
defaultExpandAllRows: false,
}}
columns={[
{
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)} />;
columns={
[
{
dataIndex: 'title',
title: t('Route name'),
},
},
] 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)}
/>
</>

View File

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

View File

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

View File

@ -55,62 +55,64 @@ export const StrategyActions = connect((props) => {
rowKey={'name'}
size={'small'}
pagination={false}
columns={[
{
dataIndex: 'displayName',
title: t('Action display name'),
render: (value) => compile(value),
},
{
dataIndex: 'onNewRecord',
title: t('Action type'),
render: (onNewRecord) =>
onNewRecord ? (
<Tag color={'green'}>{t('Action on new records')}</Tag>
) : (
<Tag color={'geekblue'}>{t('Action on existing records')}</Tag>
),
},
{
dataIndex: 'enabled',
title: t('Allow'),
render: (enabled, action) => (
<Checkbox
checked={enabled}
aria-label={`${action.name}_checkbox`}
onChange={(e) => {
if (enabled) {
delete scopes[action.name];
} else {
scopes[action.name] = 'all';
}
onChange(toFieldValue(scopes));
}}
/>
),
},
{
dataIndex: 'scope',
title: t('Data scope'),
render: (scope, action) =>
!action.onNewRecord && (
<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;
columns={
[
{
dataIndex: 'displayName',
title: t('Action display name'),
render: (value) => compile(value),
},
{
dataIndex: 'onNewRecord',
title: t('Action type'),
render: (onNewRecord) =>
onNewRecord ? (
<Tag color={'green'}>{t('Action on new records')}</Tag>
) : (
<Tag color={'geekblue'}>{t('Action on existing records')}</Tag>
),
},
{
dataIndex: 'enabled',
title: t('Allow'),
render: (enabled, action) => (
<Checkbox
checked={enabled}
aria-label={`${action.name}_checkbox`}
onChange={(e) => {
if (enabled) {
delete scopes[action.name];
} else {
scopes[action.name] = 'all';
}
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) => {
let scope = 'all';
let enabled = false;

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-action-bulk-edit",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"main": "dist/server/index.js",
"homepage": "https://docs.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",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"main": "dist/server/index.js",
"homepage": "https://docs.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 { isValid } from '@formily/shared';
import {
ActionDesigner,
SchemaSettings,
@ -19,6 +18,8 @@ import {
useDesignable,
useSchemaToolbar,
RefreshDataBlockRequest,
useAfterSuccessOptions,
useGlobalVariable,
} from '@nocobase/client';
import { useTranslation } from 'react-i18next';
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() {
const { dn } = useDesignable();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const environmentVariables = useGlobalVariable('$env');
return (
<SchemaSettingsModalItem
title={t('After successful submission')}
@ -100,8 +112,9 @@ function AfterSuccess() {
redirectTo: {
title: t('Link'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
'x-component': 'Variable.TextArea',
// eslint-disable-next-line react-hooks/rules-of-hooks
'x-use-component-props': () => useVariableProps(environmentVariables),
},
},
} as ISchema

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-action-custom-request",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"main": "dist/server/index.js",
"homepage": "https://docs.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',
});
if (res.headers['content-disposition']) {
const regex = /attachment;\s*filename="([^"]+)"/;
const match = res.headers['content-disposition'].match(regex);
if (match?.[1]) {
saveAs(res.data, match[1]);
const contentDisposition = res.headers['content-disposition'];
const utf8Match = contentDisposition.match(/filename\*=utf-8''([^;]+)/i);
const asciiMatch = contentDisposition.match(/filename="([^"]+)"/i);
if (utf8Match) {
saveAs(res.data, decodeURIComponent(utf8Match[1]));
} else if (asciiMatch) {
saveAs(res.data, asciiMatch[1]);
}
}
actionField.data.loading = false;

View File

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

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "操作:导入记录",
"description": "Import records using excel templates. You can configure which fields to import and templates will be generated automatically.",
"description.zh-CN": "使用 Excel 模板导入数据,可以配置导入哪些字段,自动生成模板。",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"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 React, { useState } from 'react';
import { NAMESPACE } from './constants';
import { useTranslation } from 'react-i18next';
import { UploadOutlined } from '@ant-design/icons';
const importFormSchema: ISchema = {
@ -128,7 +129,7 @@ export const ImportAction = (props) => {
return (
<ActionContextProvider value={{ visible, setVisible, fieldSchema }}>
<Action
icon={props.icon || <UploadOutlined />}
icon={props.icon || 'uploadoutlined'}
title={compile(fieldSchema?.title || "t('Import')")}
{...props}
onClick={() => setVisible(true)}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-audit-logs",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"displayName": "Audit logs (deprecated)",
"displayName.zh-CN": "审计日志(废弃)",
"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": "认证:短信",
"description": "SMS authentication.",
"description.zh-CN": "通过短信验证码认证身份。",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"main": "./dist/server/index.js",
"homepage": "https://docs.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",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/auth",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/auth",

View File

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

View File

@ -111,11 +111,11 @@ const LearnMore: any = (props: { collectionsData?: any; isBackup?: boolean }) =>
render: (_, data) => {
const title = compile(data.title);
const name = data.name;
return name === title ? title : (
return name === title ? (
title
) : (
<div>
{data.name}
{' '}
<span style={{ color: 'rgba(0, 0, 0, 0.3)', fontSize: '0.9em' }}>({compile(data.title)})</span>
{data.name} <span style={{ color: 'rgba(0, 0, 0, 0.3)', fontSize: '0.9em' }}>({compile(data.title)})</span>
</div>
);
},
@ -418,73 +418,75 @@ export const BackupAndRestoreList = () => {
<Table
dataSource={dataSource}
loading={loading}
columns={[
{
title: t('Backup file'),
dataIndex: 'name',
width: 400,
onCell: (data) => {
return data.inProgress
? {
colSpan: 4,
}
: {};
columns={
[
{
title: t('Backup file'),
dataIndex: 'name',
width: 400,
onCell: (data) => {
return data.inProgress
? {
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 ? (
<div style={{ color: 'rgba(0, 0, 0, 0.88)' }}>
{name}({t('Backing up')}...)
</div>
) : (
<div>{name}</div>
{
title: t('File size'),
dataIndex: 'fileSize',
onCell: (data) => {
return data.inProgress
? {
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,
}
: {};
},
},
{
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']}
] as TableProps['columns']
}
/>
</Card>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-block-workbench",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"displayName": "Block: Action panel",
"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.",

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "数据表: SQL",
"description": "Provides SQL collection template",
"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.zh-CN": "https://docs-cn.nocobase.com/handbook/collection-sql",
"main": "dist/server/index.js",

View File

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

View File

@ -31,6 +31,8 @@ export default class extends Migration {
await this.db.sequelize.transaction(async (transaction) => {
const treeCollections = await this.getTreeCollections({ transaction });
for (const treeCollection of treeCollections) {
await treeCollection.load({ transaction });
const name = `main_${treeCollection.name}_path`;
const collectionOptions = {
name,
@ -47,35 +49,16 @@ export default class extends Migration {
},
],
};
const collectionInstance = this.db.getCollection(treeCollection.name);
const treeCollectionSchema = collectionInstance.collectionSchema();
if (this.app.db.inDialect('postgres') && treeCollectionSchema != this.app.db.options.schema) {
collectionOptions['schema'] = treeCollectionSchema;
}
this.app.db.collection(collectionOptions);
const treeExistsInDb = await this.app.db.getCollection(name).existsInDb({ transaction });
if (!treeExistsInDb) {
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;
await this.app.db.getRepository(treeCollection.name).chunk({
chunkSize: chunkSize,

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "数据源:主数据库",
"description": "NocoBase main database, supports relational databases such as PostgreSQL, MySQL, MariaDB and so on.",
"description.zh-CN": "NocoBase 主数据库,支持 PostgreSQL、MySQL、MariaDB 等关系型数据库。",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"main": "./dist/server/index.js",
"homepage": "https://docs.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",
"version": "1.7.0-alpha.4",
"version": "1.7.0-beta.9",
"main": "dist/server/index.js",
"displayName": "Data source manager",
"displayName.zh-CN": "数据源管理",

View File

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

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