Merge branch 'next' into develop

This commit is contained in:
katherinehhh 2025-02-26 17:28:41 +08:00
commit 96cc54d927
30 changed files with 550 additions and 167 deletions

View File

@ -2,9 +2,7 @@
"version": "1.6.0-alpha.28", "version": "1.6.0-alpha.28",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"npmClientArgs": [ "npmClientArgs": ["--ignore-engines"],
"--ignore-engines"
],
"command": { "command": {
"version": { "version": {
"forcePublish": true, "forcePublish": true,

View File

@ -304,30 +304,32 @@ export const ACLActionProvider = (props) => {
const collection = useCollection(); const collection = useCollection();
const recordPkValue = useRecordPkValue(); const recordPkValue = useRecordPkValue();
const resource = useResourceName(); const resource = useResourceName();
const { parseAction } = useACLRoleContext(); const { parseAction, uiButtonSchemasBlacklist } = useACLRoleContext();
const schema = useFieldSchema(); const schema = useFieldSchema();
const currentUid = schema['x-uid'];
let actionPath = schema['x-acl-action']; let actionPath = schema['x-acl-action'];
const editablePath = ['create', 'update', 'destroy', 'importXlsx']; const editablePath = ['create', 'update', 'destroy', 'importXlsx'];
if (!actionPath && resource && schema['x-action']) { if (!actionPath && resource && schema['x-action'] && editablePath.includes(schema['x-action'])) {
actionPath = `${resource}:${schema['x-action']}`; actionPath = `${resource}:${schema['x-action']}`;
} }
if (!actionPath?.includes(':')) { if (actionPath && !actionPath?.includes(':')) {
actionPath = `${resource}:${actionPath}`; actionPath = `${resource}:${actionPath}`;
} }
const params = useMemo( const params = useMemo(
() => parseAction(actionPath, { schema, recordPkValue }), () => actionPath && parseAction(actionPath, { schema, recordPkValue }),
[parseAction, actionPath, schema, recordPkValue], [parseAction, actionPath, schema, recordPkValue],
); );
if (uiButtonSchemasBlacklist.includes(currentUid)) {
return <ACLActionParamsContext.Provider value={false}>{props.children}</ACLActionParamsContext.Provider>;
}
if (!actionPath) { if (!actionPath) {
return <>{props.children}</>; return <>{props.children}</>;
} }
if (!resource) { if (!resource) {
return <>{props.children}</>; return <>{props.children}</>;
} }
if (!params) { if (!params) {
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>; return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
} }

View File

@ -14,6 +14,7 @@ export const DestroyActionInitializer = (props) => {
const schema = { const schema = {
title: '{{ t("Delete") }}', title: '{{ t("Delete") }}',
'x-action': 'destroy', 'x-action': 'destroy',
'x-acl-action': 'destroy',
'x-component': 'Action', 'x-component': 'Action',
'x-use-component-props': 'useDestroyActionProps', 'x-use-component-props': 'useDestroyActionProps',
'x-toolbar': 'ActionSchemaToolbar', 'x-toolbar': 'ActionSchemaToolbar',

View File

@ -20,6 +20,7 @@ export const LinkActionInitializer = (props) => {
'x-settings': 'actionSettings:link', 'x-settings': 'actionSettings:link',
'x-component': props?.['x-component'] || 'Action.Link', 'x-component': props?.['x-component'] || 'Action.Link',
'x-use-component-props': 'useLinkActionProps', 'x-use-component-props': 'useLinkActionProps',
'x-decorator': 'ACLActionProvider',
}; };
const itemConfig = useSchemaInitializerItem(); const itemConfig = useSchemaInitializerItem();

View File

@ -16,7 +16,11 @@ import { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
import { useCollection_deprecated } from '../../../collection-manager'; import { useCollection_deprecated } from '../../../collection-manager';
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
import { SchemaSettingsLinkageRules, SchemaSettingsModalItem } from '../../../schema-settings'; import {
SchemaSettingsLinkageRules,
SchemaSettingsModalItem,
SchemaSettingAccessControl,
} from '../../../schema-settings';
import { useURLAndHTMLSchema } from './useURLAndHTMLSchema'; import { useURLAndHTMLSchema } from './useURLAndHTMLSchema';
export const SchemaSettingsActionLinkItem: FC = () => { export const SchemaSettingsActionLinkItem: FC = () => {
@ -103,6 +107,7 @@ export const customizeLinkActionSettings = new SchemaSettings({
}; };
}, },
}, },
SchemaSettingAccessControl,
{ {
name: 'remove', name: 'remove',
sort: 100, sort: 100,

View File

@ -28,6 +28,7 @@ export const PopupActionInitializer = (props) => {
openMode: defaultOpenMode, openMode: defaultOpenMode,
refreshDataBlockRequest: true, refreshDataBlockRequest: true,
}, },
'x-decorator': 'ACLActionProvider',
properties: { properties: {
drawer: { drawer: {
type: 'void', type: 'void',

View File

@ -20,6 +20,7 @@ export const UpdateActionInitializer = (props) => {
type: 'void', type: 'void',
title: '{{ t("Edit") }}', title: '{{ t("Edit") }}',
'x-action': 'update', 'x-action': 'update',
'x-acl-action': 'update',
'x-toolbar': 'ActionSchemaToolbar', 'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:edit', 'x-settings': 'actionSettings:edit',
'x-component': 'Action', 'x-component': 'Action',

View File

@ -14,7 +14,7 @@ import { useCollection_deprecated } from '../../../collection-manager';
import { useCollection } from '../../../data-source'; import { useCollection } from '../../../data-source';
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
import { SchemaSettingsLinkageRules } from '../../../schema-settings'; import { SchemaSettingsLinkageRules, SchemaSettingAccessControl } from '../../../schema-settings';
import { useOpenModeContext } from '../../popup/OpenModeProvider'; import { useOpenModeContext } from '../../popup/OpenModeProvider';
import { useCurrentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider'; import { useCurrentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
@ -57,6 +57,7 @@ export const customizePopupActionSettings = new SchemaSettings({
}; };
}, },
}, },
SchemaSettingAccessControl,
{ {
name: 'remove', name: 'remove',
sort: 100, sort: 100,

View File

@ -29,20 +29,12 @@ export class PMPlugin extends Plugin {
} }
addSettings() { addSettings() {
// this.app.pluginSettingsManager.add('acl', { this.app.pluginSettingsManager.add('ui-schema-storage', {
// title: '{{t("Access control")}}', title: '{{t("Block templates")}}',
// icon: 'LockOutlined', icon: 'LayoutOutlined',
// Component: ACLPane, Component: BlockTemplatesPane,
// aclSnippet: 'pm.acl.roles', aclSnippet: 'pm.ui-schema-storage.block-templates',
// }); });
// Replaced by plugin-block-template
// this.app.pluginSettingsManager.add('ui-schema-storage', {
// title: '{{t("Block templates")}}',
// icon: 'LayoutOutlined',
// Component: BlockTemplatesPane,
// aclSnippet: 'pm.ui-schema-storage.block-templates',
// });
this.app.pluginSettingsManager.add('system-settings', { this.app.pluginSettingsManager.add('system-settings', {
icon: 'SettingOutlined', icon: 'SettingOutlined',
title: '{{t("System settings")}}', title: '{{t("System settings")}}',

View File

@ -0,0 +1,86 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useFieldSchema } from '@formily/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { App } from 'antd';
import { SchemaSettingsActionModalItem } from './SchemaSettings';
import { useAPIClient } from '../api-client/hooks/useAPIClient';
import { useRequest } from '../api-client';
import { useACLContext } from '../acl';
export function AccessControl() {
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const apiClient = useAPIClient();
const resource = apiClient.resource('uiSchemas.roles', fieldSchema['x-uid']);
const { message } = App.useApp();
const { refresh, data }: any = useRequest(
{
url: `/uiSchemas/${fieldSchema['x-uid']}/roles:list`,
},
{
manual: true,
},
);
const { refresh: refreshRoleCheck } = useACLContext();
const AccessControl = (
<SchemaSettingsActionModalItem
scope={t}
title={t('Access control')}
schema={{
type: 'object',
properties: {
roles: {
type: 'array',
title: t('Roles'),
'x-decorator': 'FormItem',
'x-decorator-props': {
tooltip: t('If not set, all roles can see this action'),
},
'x-component': 'RemoteSelect',
'x-component-props': {
multiple: true,
objectValue: true,
dataSource: 'main',
service: {
resource: 'roles',
},
manual: false,
fieldNames: {
label: 'title',
value: 'name',
},
},
},
},
}}
initialValues={{
roles: data?.data,
}}
beforeOpen={() => !data && refresh()}
onSubmit={async ({ roles }) => {
await resource.set({ values: roles.map((v) => v.name) });
await refreshRoleCheck();
return message.success(t('Saved successfully'));
}}
/>
);
return AccessControl;
}
export const SchemaSettingAccessControl = {
name: 'accessControl',
Component: AccessControl,
useVisible() {
const fieldSchema = useFieldSchema();
return fieldSchema['x-decorator'] === 'ACLActionProvider';
},
};

View File

@ -725,8 +725,8 @@ export const SchemaSettingsActionModalItem: FC<SchemaSettingsActionModalItemProp
} }
return result; return result;
}, {}); }, {});
await onSubmit?.(cloneDeep(visibleValues));
setVisible(false); setVisible(false);
await onSubmit?.(cloneDeep(visibleValues));
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }

View File

@ -0,0 +1,68 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { expect, test } from '@nocobase/test/e2e';
import { accessControlActionWithTable } from './template';
test.describe('Access control', () => {
test('popup、link、custom request support access control', async ({ page, mockPage, mockRecord }) => {
const nocoPage = await mockPage(accessControlActionWithTable).waitForInit();
await nocoPage.goto();
await page.getByLabel('block-item-CardItem-users-').hover();
//popup
await page.getByLabel('action-Action-Popup-customize').hover();
await page.getByLabel('designer-schema-settings-Action-actionSettings:popup-users').hover();
await expect(page.getByRole('menuitem', { name: 'Access control' })).toBeVisible();
await page.getByLabel('designer-schema-settings-Action-actionSettings:popup-users').hover();
await page.mouse.move(300, 0);
//link
await page.getByLabel('action-Action-Link-customize:').hover();
await page.getByLabel('designer-schema-settings-Action-actionSettings:link-users').hover();
await expect(page.getByRole('menuitem', { name: 'Access control' })).toBeVisible();
await page.mouse.move(300, 0);
// custom request
await page.getByLabel('action-CustomRequestAction-').hover();
await page.getByLabel('designer-schema-settings-CustomRequestAction-actionSettings:customRequest-users').hover();
await expect(page.getByRole('menuitem', { name: 'Access control' })).toBeVisible();
await page.mouse.move(300, 0);
});
test('access control with role ', async ({ page, mockPage, mockRecord }) => {
const nocoPage = await mockPage(accessControlActionWithTable).waitForInit();
await nocoPage.goto();
await page.getByLabel('block-item-CardItem-users-').hover();
//popup only member can see
await page.getByLabel('action-Action-Popup-customize').hover();
await page.getByLabel('designer-schema-settings-Action-actionSettings:popup-users').hover();
await page.getByRole('menuitem', { name: 'Access control' }).click();
await page.getByLabel('block-item-RemoteSelect-users').click();
await page.getByText('Member').click();
await page.getByRole('option', { name: 'Member' }).locator('div').click();
await page.getByLabel('block-item-RemoteSelect-users').click();
await page.getByRole('button', { name: 'Submit' }).click();
//root 角色有权限
await expect(page.getByLabel('action-Action-Popup-customize')).toBeVisible();
//切换 为admin
await page.getByTestId('user-center-button').click();
await page.getByText('Switch roleRoot').click();
await page.getByText('Admin', { exact: true }).click();
await expect(page.getByLabel('action-Action-Popup-customize')).not.toBeVisible();
// 切换 为 member
await page.getByTestId('user-center-button').click();
await page.getByText('Switch roleAdmin').click();
await page.getByText('Member').click();
await expect(page.getByLabel('action-Action-Popup-customize')).toBeVisible();
});
});

View File

@ -1964,3 +1964,254 @@ export const whenClearingARelationshipFieldTheValueOfTheAssociatedFieldShouldBeC
'x-index': 1, 'x-index': 1,
}, },
}; };
export const accessControlActionWithTable = {
pageSchema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Page',
'x-app-version': '1.6.0-beta.9',
properties: {
fvgd0c2akgf: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'page:addBlock',
'x-app-version': '1.6.0-beta.9',
properties: {
'0c4zy47hhyq': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '1.6.0-beta.9',
properties: {
b1y881c771g: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '1.6.0-beta.9',
properties: {
hccefuo80kx: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action': 'users:view',
'x-decorator': 'DetailsBlockProvider',
'x-use-decorator-props': 'useDetailsWithPaginationDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'users',
readPretty: true,
action: 'list',
params: {
pageSize: 1,
},
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:detailsWithPagination',
'x-component': 'CardItem',
'x-app-version': '1.6.0-beta.9',
properties: {
fctprt7i7ut: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Details',
'x-read-pretty': true,
'x-use-component-props': 'useDetailsWithPaginationProps',
'x-app-version': '1.6.0-beta.9',
properties: {
b2xrbq4a060: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'details:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 24,
},
},
'x-app-version': '1.6.0-beta.9',
properties: {
e2ccvx3c7o5: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Popup") }}',
'x-action': 'customize:popup',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:popup',
'x-component': 'Action',
'x-component-props': {
openMode: 'drawer',
refreshDataBlockRequest: true,
},
'x-decorator': 'ACLActionProvider',
'x-action-context': {
dataSource: 'main',
collection: 'users',
},
'x-app-version': '1.6.0-beta.9',
properties: {
drawer: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Popup") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
'x-app-version': '1.6.0-beta.9',
properties: {
tabs: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-initializer': 'popup:addTab',
'x-app-version': '1.6.0-beta.9',
properties: {
tab1: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{t("Details")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
'x-app-version': '1.6.0-beta.9',
properties: {
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'popup:common:addBlock',
'x-app-version': '1.6.0-beta.9',
'x-uid': '3qfg5qfvsth',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'n0574ydwdua',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'm0039599o9q',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '5t7ol82dt74',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'gn76wlmk619',
'x-async': false,
'x-index': 1,
},
'3jat4vour4y': {
_isJSONSchemaObject: true,
version: '2.0',
title: '{{ t("Custom request") }}',
'x-component': 'CustomRequestAction',
'x-action': 'customize:form:request',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:customRequest',
'x-decorator': 'CustomRequestAction.Decorator',
'x-action-settings': {
onSuccess: {
manualClose: false,
redirecting: false,
successMessage: '{{t("Request success")}}',
},
},
type: 'void',
'x-app-version': '1.6.0-beta.9',
'x-uid': 'gu0shkseqoa',
'x-async': false,
'x-index': 2,
},
'0tevnuro5d4': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Link") }}',
'x-action': 'customize:link',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:link',
'x-component': 'Action',
'x-use-component-props': 'useLinkActionProps',
'x-decorator': 'ACLActionProvider',
'x-app-version': '1.6.0-beta.9',
'x-uid': 'swtrz2mpnm4',
'x-async': false,
'x-index': 3,
},
},
'x-uid': '32u6fnlj0ti',
'x-async': false,
'x-index': 1,
},
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'details:configureFields',
'x-app-version': '1.6.0-beta.9',
'x-uid': '21ksski5wgs',
'x-async': false,
'x-index': 2,
},
pagination: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Pagination',
'x-use-component-props': 'useDetailsPaginationProps',
'x-app-version': '1.6.0-beta.9',
'x-uid': '2cz3ilk7hmv',
'x-async': false,
'x-index': 3,
},
},
'x-uid': 'f6xosucy75q',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '33tsqeap83o',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ppglkb7uvt8',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'af78vam04ux',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'askp9xe9uag',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'huon5vcb8u8',
'x-async': true,
'x-index': 1,
},
};

View File

@ -26,6 +26,7 @@ export * from './SchemaSettingsRenderEngine';
export * from './hooks/useGetAriaLabelOfDesigner'; export * from './hooks/useGetAriaLabelOfDesigner';
export * from './hooks/useIsAllowToSetDefaultValue'; export * from './hooks/useIsAllowToSetDefaultValue';
export * from './SchemaSettingsLayoutItem'; export * from './SchemaSettingsLayoutItem';
export * from './SchemaSettingAccessControl';
export { default as useParseDataScopeFilter } from './hooks/useParseDataScopeFilter'; export { default as useParseDataScopeFilter } from './hooks/useParseDataScopeFilter';
export * from './isPatternDisabled'; export * from './isPatternDisabled';
export { SchemaSettingsPlugin } from './SchemaSettingsPlugin'; export { SchemaSettingsPlugin } from './SchemaSettingsPlugin';

View File

@ -43,6 +43,23 @@ export async function checkAction(ctx, next) {
} }
const availableActions = ctx.app.acl.getAvailableActions(); const availableActions = ctx.app.acl.getAvailableActions();
let uiButtonSchemasBlacklist = [];
if (currentRole !== 'root') {
const eqCurrentRoleList = await ctx.db
.getRepository('uiButtonSchemasRoles')
.find({
filter: { 'roleName.$eq': currentRole },
})
.then((list) => list.map((v) => v.uid));
const NECurrentRoleList = await ctx.db
.getRepository('uiButtonSchemasRoles')
.find({
filter: { 'roleName.$ne': currentRole },
})
.then((list) => list.map((v) => v.uid));
uiButtonSchemasBlacklist = NECurrentRoleList.filter((uid) => !eqCurrentRoleList.includes(uid));
}
ctx.body = { ctx.body = {
...role.toJSON(), ...role.toJSON(),
@ -53,6 +70,7 @@ export async function checkAction(ctx, next) {
allowConfigure: roleInstance.get('allowConfigure'), allowConfigure: roleInstance.get('allowConfigure'),
allowMenuItemIds: roleInstance.get('menuUiSchemas').map((uiSchema) => uiSchema.get('x-uid')), allowMenuItemIds: roleInstance.get('menuUiSchemas').map((uiSchema) => uiSchema.get('x-uid')),
allowAnonymous: !!anonymous, allowAnonymous: !!anonymous,
uiButtonSchemasBlacklist,
}; };
await next(); await next();

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { Action, useAPIClient, useRequest, withDynamicSchemaProps } from '@nocobase/client'; import { Action, useAPIClient, useRequest, withDynamicSchemaProps, ACLActionProvider } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { listByCurrentRoleUrl } from '../constants'; import { listByCurrentRoleUrl } from '../constants';
@ -16,23 +16,23 @@ import { CustomRequestActionDesigner } from './CustomRequestActionDesigner';
export const CustomRequestActionACLDecorator = (props) => { export const CustomRequestActionACLDecorator = (props) => {
const apiClient = useAPIClient(); const apiClient = useAPIClient();
const isRoot = apiClient.auth.role === 'root'; // const isRoot = apiClient.auth.role === 'root';
const fieldSchema = useFieldSchema(); // const fieldSchema = useFieldSchema();
const { data } = useRequest<{ data: string[] }>( // const { data } = useRequest<{ data: string[] }>(
{ // {
url: listByCurrentRoleUrl, // url: listByCurrentRoleUrl,
}, // },
{ // {
manual: isRoot, // manual: isRoot,
cacheKey: listByCurrentRoleUrl, // cacheKey: listByCurrentRoleUrl,
}, // },
); // );
const requestId = fieldSchema?.['x-custom-request-id'] || fieldSchema?.['x-uid'];
if (!isRoot && !data?.data?.includes(requestId)) {
return null;
}
return props.children; // // if (!isRoot && !data?.data?.includes(fieldSchema?.['x-uid'])) {
// // return null;
// // }
return <ACLActionProvider>{props.children}</ACLActionProvider>;
}; };
const components = { const components = {

View File

@ -17,15 +17,13 @@ import {
useCollection_deprecated, useCollection_deprecated,
useDataSourceKey, useDataSourceKey,
useDesignable, useDesignable,
useRequest, SchemaSettingAccessControl,
} from '@nocobase/client'; } from '@nocobase/client';
import { App } from 'antd';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { listByCurrentRoleUrl } from '../constants';
import { useCustomRequestVariableOptions, useGetCustomRequest } from '../hooks'; import { useCustomRequestVariableOptions, useGetCustomRequest } from '../hooks';
import { useCustomRequestsResource } from '../hooks/useCustomRequestsResource'; import { useCustomRequestsResource } from '../hooks/useCustomRequestsResource';
import { useTranslation } from '../locale'; import { useTranslation } from '../locale';
import { CustomRequestACLSchema, CustomRequestConfigurationFieldsSchema } from '../schemas'; import { CustomRequestConfigurationFieldsSchema } from '../schemas';
export function CustomRequestSettingsItem() { export function CustomRequestSettingsItem() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -89,63 +87,6 @@ export function CustomRequestSettingsItem() {
); );
} }
export function CustomRequestACL() {
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const customRequestsResource = useCustomRequestsResource();
const { message } = App.useApp();
const { data, refresh } = useGetCustomRequest();
const { dn } = useDesignable();
const { refresh: refreshRoleCustomKeys } = useRequest<{ data: string[] }>(
{
url: listByCurrentRoleUrl,
},
{
manual: true,
cacheKey: listByCurrentRoleUrl,
},
);
return (
<>
<SchemaSettingsActionModalItem
title={t('Access control')}
schema={CustomRequestACLSchema}
initialValues={{
roles: data?.data?.roles,
}}
beforeOpen={() => !data && refresh()}
onSubmit={async ({ roles }) => {
const isSelfRequest =
!fieldSchema['x-custom-request-id'] || fieldSchema['x-custom-request-id'] === fieldSchema['x-uid'];
if (!isSelfRequest) {
fieldSchema['x-custom-request-id'] = fieldSchema['x-uid'];
await dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-custom-request-id': fieldSchema['x-uid'],
},
});
}
await customRequestsResource.updateOrCreate({
values: {
key: fieldSchema['x-uid'],
roles,
},
filterKeys: ['key'],
});
refresh();
refreshRoleCustomKeys();
dn.refresh();
return message.success(t('Saved successfully'));
}}
/>
</>
);
}
/** /**
* @deprecated * @deprecated
*/ */
@ -160,10 +101,7 @@ export const customRequestActionSettings = new SchemaSettings({
name: 'request settings', name: 'request settings',
Component: CustomRequestSettingsItem, Component: CustomRequestSettingsItem,
}, },
{ SchemaSettingAccessControl,
name: 'accessControl',
Component: CustomRequestACL,
},
], ],
}, },
], ],

View File

@ -19,8 +19,9 @@ import {
useCollection, useCollection,
useCollectionRecord, useCollectionRecord,
useSchemaToolbar, useSchemaToolbar,
SchemaSettingAccessControl,
} from '@nocobase/client'; } from '@nocobase/client';
import { CustomRequestACL, CustomRequestSettingsItem } from './components/CustomRequestActionDesigner'; import { CustomRequestSettingsItem } from './components/CustomRequestActionDesigner';
export const customizeCustomRequestActionSettings = new SchemaSettings({ export const customizeCustomRequestActionSettings = new SchemaSettings({
name: 'actionSettings:customRequest', name: 'actionSettings:customRequest',
@ -64,8 +65,10 @@ export const customizeCustomRequestActionSettings = new SchemaSettings({
Component: CustomRequestSettingsItem, Component: CustomRequestSettingsItem,
}, },
{ {
name: 'accessControl', ...SchemaSettingAccessControl,
Component: CustomRequestACL, useVisible() {
return true;
},
}, },
{ {
name: 'refreshDataBlockRequest', name: 'refreshDataBlockRequest',

View File

@ -1,39 +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.
*/
import { DEFAULT_DATA_SOURCE_KEY } from '@nocobase/client';
import { generateNTemplate } from '../locale';
export const CustomRequestACLSchema = {
type: 'object',
properties: {
roles: {
type: 'array',
title: generateNTemplate('Roles'),
'x-decorator': 'FormItem',
'x-decorator-props': {
tooltip: generateNTemplate('If not set, all roles can see this action'),
},
'x-component': 'RemoteSelect',
'x-component-props': {
multiple: true,
objectValue: true,
dataSource: DEFAULT_DATA_SOURCE_KEY,
service: {
resource: 'roles',
},
manual: false,
fieldNames: {
label: 'title',
value: 'name',
},
},
},
},
};

View File

@ -8,4 +8,3 @@
*/ */
export * from './CustomRequestConfigurationFields'; export * from './CustomRequestConfigurationFields';
export * from './CustomRequestACL';

View File

@ -8,7 +8,7 @@
*/ */
import { useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { Action, Icon, useCompile, useComponent, withDynamicSchemaProps } from '@nocobase/client'; import { Action, Icon, useCompile, useComponent, withDynamicSchemaProps, ACLActionProvider } from '@nocobase/client';
import { Avatar } from 'antd'; import { Avatar } from 'antd';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
@ -46,9 +46,9 @@ function Button() {
const compile = useCompile(); const compile = useCompile();
const title = compile(fieldSchema.title); const title = compile(fieldSchema.title);
return layout === WorkbenchLayout.Grid ? ( return layout === WorkbenchLayout.Grid ? (
<div title={fieldSchema.title} className={cx(styles.avatar)}> <div title={title} className={cx(styles.avatar)}>
<Avatar style={{ backgroundColor }} size={48} icon={<Icon type={icon} />} /> <Avatar style={{ backgroundColor }} size={48} icon={<Icon type={icon} />} />
<div className={cx(styles.title)}>{fieldSchema.title}</div> <div className={cx(styles.title)}>{title}</div>
</div> </div>
) : ( ) : (
<span>{title}</span> <span>{title}</span>
@ -61,13 +61,15 @@ export const WorkbenchAction = withDynamicSchemaProps((props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const Component = useComponent(props?.targetComponent) || Action; const Component = useComponent(props?.targetComponent) || Action;
return ( return (
<Component <ACLActionProvider>
className={cx(className, styles.action, 'nb-action-panel')} <Component
{...others} className={cx(className, styles.action, 'nb-action-panel')}
type="text" {...others}
icon={null} type="text"
title={<Button />} icon={null}
confirmTitle={fieldSchema.title} title={<Button />}
/> confirmTitle={fieldSchema.title}
/>
</ACLActionProvider>
); );
}); });

View File

@ -13,6 +13,7 @@ import {
SchemaSettingsActionLinkItem, SchemaSettingsActionLinkItem,
useSchemaInitializer, useSchemaInitializer,
ModalActionSchemaInitializerItem, ModalActionSchemaInitializerItem,
SchemaSettingAccessControl,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -30,6 +31,12 @@ export const workbenchActionSettingsCustomRequest = new SchemaSettings({
name: 'editLink', name: 'editLink',
Component: SchemaSettingsActionLinkItem, Component: SchemaSettingsActionLinkItem,
}, },
{
...SchemaSettingAccessControl,
useVisible() {
return true;
},
},
{ {
sort: 800, sort: 800,
name: 'd1', name: 'd1',

View File

@ -14,6 +14,7 @@ import {
useSchemaInitializer, useSchemaInitializer,
useSchemaInitializerItem, useSchemaInitializerItem,
ModalActionSchemaInitializerItem, ModalActionSchemaInitializerItem,
SchemaSettingAccessControl,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -32,6 +33,12 @@ export const workbenchActionSettingsLink = new SchemaSettings({
name: 'editLink', name: 'editLink',
Component: SchemaSettingsActionLinkItem, Component: SchemaSettingsActionLinkItem,
}, },
{
...SchemaSettingAccessControl,
useVisible() {
return true;
},
},
{ {
sort: 800, sort: 800,
name: 'd1', name: 'd1',

View File

@ -14,6 +14,7 @@ import {
useSchemaInitializer, useSchemaInitializer,
useOpenModeContext, useOpenModeContext,
ModalActionSchemaInitializerItem, ModalActionSchemaInitializerItem,
SchemaSettingAccessControl,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -44,6 +45,12 @@ export const workbenchActionSettingsPopup = new SchemaSettings({
}; };
}, },
}, },
{
...SchemaSettingAccessControl,
useVisible() {
return true;
},
},
{ {
sort: 800, sort: 800,
name: 'd1', name: 'd1',

View File

@ -14,6 +14,7 @@ import {
useSchemaInitializer, useSchemaInitializer,
useSchemaInitializerItem, useSchemaInitializerItem,
ModalActionSchemaInitializerItem, ModalActionSchemaInitializerItem,
SchemaSettingAccessControl,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -28,6 +29,12 @@ export const workbenchActionSettingsScanQrCode = new SchemaSettings({
return { hasIconColor: true }; return { hasIconColor: true };
}, },
}, },
{
...SchemaSettingAccessControl,
useVisible() {
return true;
},
},
{ {
name: 'd1', name: 'd1',
type: 'divider', type: 'divider',

View File

@ -264,7 +264,7 @@ describe('view collection', function () {
const Role = await collectionRepository.create({ const Role = await collectionRepository.create({
values: { values: {
name: 'roles', name: 'my_roles',
fields: [{ name: 'name', type: 'string' }], fields: [{ name: 'name', type: 'string' }],
}, },
context: {}, context: {},
@ -276,7 +276,7 @@ describe('view collection', function () {
values: [{ name: 'u1' }, { name: 'u2' }], values: [{ name: 'u1' }, { name: 'u2' }],
}); });
await db.getRepository('roles').create({ await db.getRepository('my_roles').create({
values: [{ name: 'r1' }, { name: 'r2' }], values: [{ name: 'r1' }, { name: 'r2' }],
}); });
@ -327,7 +327,7 @@ describe('view collection', function () {
collectionName: 'users', collectionName: 'users',
name: 'roles', name: 'roles',
type: 'belongsToMany', type: 'belongsToMany',
target: 'roles', target: 'my_roles',
through: 'test_view', through: 'test_view',
foreignKey: 'user_id', foreignKey: 'user_id',
otherKey: 'role_id', otherKey: 'role_id',

View File

@ -8,7 +8,6 @@
*/ */
import { Plugin } from '@nocobase/client'; import { Plugin } from '@nocobase/client';
class PluginUISchemaStorageClient extends Plugin { class PluginUISchemaStorageClient extends Plugin {
async load() {} async load() {}
} }

View File

@ -0,0 +1,16 @@
/**
* 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 { defineCollection } from '@nocobase/database';
export default defineCollection({
name: 'uiButtonSchemasRoles',
dumpRules: 'required',
migrationRules: ['overwrite', 'schema-only'],
});

View File

@ -39,5 +39,16 @@ export default {
name: 'schema', name: 'schema',
defaultValue: {}, defaultValue: {},
}, },
{
type: 'belongsToMany',
name: 'roles',
onDelete: 'CASCADE',
through: 'uiButtonSchemasRoles',
target: 'roles',
foreignKey: 'uid',
otherKey: 'roleName',
sourceKey: 'x-uid',
targetKey: 'name',
},
], ],
} as CollectionOptions; } as CollectionOptions;

View File

@ -42,7 +42,7 @@ export class PluginUISchemaStorageServer extends Plugin {
this.app.acl.registerSnippet({ this.app.acl.registerSnippet({
name: 'ui.uiSchemas', name: 'ui.uiSchemas',
actions: ['uiSchemas:*'], actions: ['uiSchemas:*', 'uiSchemas.roles:list', 'uiSchemas.roles:set'],
}); });
db.on('uiSchemas.beforeCreate', function setUid(model) { db.on('uiSchemas.beforeCreate', function setUid(model) {