feat(client): improve acl module

This commit is contained in:
chenos 2022-02-26 23:03:58 +08:00
parent 6f2069e918
commit d1ac62ddaf
10 changed files with 310 additions and 47 deletions

View File

@ -3,6 +3,8 @@ export interface AvailableActionOptions {
type: 'new-data' | 'old-data';
displayName?: string;
resource?: string;
onNewRecord?: boolean;
allowConfigureFields?: boolean;
}
export class AclAvailableAction {

View File

@ -47,7 +47,7 @@ export const RoleConfigure = () => {
'x-component': 'Checkbox',
'x-content': '允许配置系统,包括界面配置、数据表配置、权限配置、系统配置等全部配置项',
},
strategy: {
'strategy.actions': {
title: '通用数据操作权限',
description: '所有数据表都默认使用通用数据操作权限;同时,可以针对每个数据表单独配置权限。',
'x-component': 'StrategyActions',

View File

@ -1,15 +1,62 @@
import { FormItem, FormLayout } from '@formily/antd';
import { Checkbox, Select, Table } from 'antd';
import { ArrayField } from '@formily/core';
import { connect, useField, useForm } from '@formily/react';
import { Checkbox, Select, Table, Tag } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useAvailableActions } from '.';
import { useCollectionManager, useCompile, useRecord } from '../..';
export const RolesResourcesActions = () => {
const toActionMap = (arr: any[]) => {
const obj = {};
arr?.forEach((action) => {
obj[action.name] = action;
});
return obj;
};
export const RolesResourcesActions = connect((props) => {
const { onChange } = props;
const form = useForm();
const roleCollection = useRecord();
const availableActions = useAvailableActions();
const { getCollection } = useCollectionManager();
const collection = getCollection(roleCollection.name);
const compile = useCompile();
const { t } = useTranslation();
const field = useField<ArrayField>();
const actionMap: any = toActionMap(field.value || []);
const inAction = (actionName, fieldName) => {
const action = actionMap?.[actionName];
if (!action) {
return false;
}
return action?.fields?.includes(fieldName);
};
const availableActionsWithFields = availableActions.filter((action) => action.allowConfigureFields);
const fieldPermissions = collection?.fields?.map((field) => {
const permission = { ...field };
for (const action of availableActionsWithFields) {
permission[action.name] = inAction(action.name, field.name);
}
return permission;
});
const toggleAction = (actionName: string) => {
if (actionMap[actionName]) {
delete actionMap[actionName];
} else {
actionMap[actionName] = {
name: actionName,
fields: collection?.fields?.map?.((item) => item.name),
};
}
onChange(Object.values(actionMap));
};
const allChecked = {};
for (const action of availableActionsWithFields) {
allChecked[action.name] = collection?.fields?.length === actionMap?.[action.name]?.fields?.length;
}
return (
<div>
<FormLayout layout={'vertical'}>
@ -24,13 +71,26 @@ export const RolesResourcesActions = () => {
render: (value) => compile(value),
},
{
dataIndex: 'type',
dataIndex: 'onNewRecord',
title: '类型',
render: (onNewRecord) =>
onNewRecord ? (
<Tag color={'green'}>{t('Operate on new record')}</Tag>
) : (
<Tag color={'geekblue'}>{t('Operate on existing record')}</Tag>
),
},
{
dataIndex: 'enable',
dataIndex: 'enabled',
title: '允许操作',
render: () => <Checkbox />,
render: (enabled, action) => (
<Checkbox
checked={enabled}
onChange={() => {
toggleAction(action.name);
}}
/>
),
},
{
dataIndex: 'scope',
@ -38,36 +98,81 @@ export const RolesResourcesActions = () => {
render: () => <Select size={'small'} />,
},
]}
dataSource={availableActions}
dataSource={availableActions?.map((item) => {
let enabled = false;
let scope = null;
if (actionMap[item.name]) {
enabled = true;
scope = actionMap[item.name]['scope'];
}
return {
...item,
enabled,
scope,
};
})}
/>
</FormItem>
<FormItem label={'字段权限'}>
<Table
dataSource={collection?.fields}
pagination={false}
dataSource={fieldPermissions}
columns={[
{
dataIndex: ['uiSchema', 'title'],
title: '字段名称',
render: (value) => compile(value),
},
{
dataIndex: 'view',
title: '查看',
render: () => <Checkbox />,
},
{
dataIndex: 'update',
title: '编辑',
render: () => <Checkbox />,
},
{
dataIndex: 'create',
title: '添加',
render: () => <Checkbox />,
},
...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 = collection?.fields?.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) {
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));
}}
/>
),
};
}),
]}
/>
</FormItem>
</FormLayout>
</div>
);
};
});

View File

@ -1,11 +1,45 @@
import { Checkbox, Select, Table } from 'antd';
import { ArrayField } from '@formily/core';
import { connect, useField } from '@formily/react';
import { Checkbox, Select, Table, Tag } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useAvailableActions } from '.';
import { useCompile } from '../..';
export const StrategyActions = () => {
const toScopes = (value) => {
if (!value) {
return {};
}
const scopes = {};
value?.forEach?.((item) => {
const [name, scope] = item.split(':');
scopes[name] = scope || 'all';
});
return scopes;
};
const toFieldValue = (scopes) => {
const values = [];
for (const name in scopes) {
if (Object.prototype.hasOwnProperty.call(scopes, name)) {
const scope = scopes[name];
if (scope === 'all') {
values.push(name);
} else {
values.push(`${name}:${scope}`);
}
}
}
return values;
};
export const StrategyActions = connect((props) => {
const { onChange } = props;
const availableActions = useAvailableActions();
const compile = useCompile();
const { t } = useTranslation();
const field = useField<ArrayField>();
const scopes = toScopes(field.value);
return (
<div>
<Table
@ -18,22 +52,66 @@ export const StrategyActions = () => {
render: (value) => compile(value),
},
{
dataIndex: 'type',
dataIndex: 'onNewRecord',
title: '类型',
render: (onNewRecord) =>
onNewRecord ? (
<Tag color={'green'}>{t('Operate on new record')}</Tag>
) : (
<Tag color={'geekblue'}>{t('Operate on existing record')}</Tag>
),
},
{
dataIndex: 'enable',
dataIndex: 'enabled',
title: '允许操作',
render: () => <Checkbox />,
render: (enabled, action) => (
<Checkbox
checked={enabled}
onChange={(e) => {
if (enabled) {
delete scopes[action.name];
} else {
scopes[action.name] = 'all';
}
onChange(toFieldValue(scopes));
}}
/>
),
},
{
dataIndex: 'scope',
title: '可操作的数据范围',
render: () => <Select size={'small'} />,
render: (scope, action) =>
!action.onNewRecord && (
<Select
size={'small'}
value={scope}
options={[
{ label: 'All records', value: 'all' },
{ label: 'Own records', value: 'own' },
]}
onChange={(value) => {
scopes[action.name] = value;
onChange(toFieldValue(scopes));
}}
/>
),
},
]}
dataSource={availableActions}
dataSource={availableActions?.map((item) => {
let scope = 'all';
let enabled = false;
if (scopes[item.name]) {
enabled = true;
scope = scopes[item.name];
}
return {
...item,
enabled,
scope,
};
})}
/>
</div>
);
};
});

View File

@ -29,6 +29,20 @@ const collection = {
description: '使用英文',
} as ISchema,
},
{
type: 'string',
name: 'usingConfig',
interface: 'input',
uiSchema: {
title: '权限策略',
type: 'string',
'x-component': 'Select',
enum: [
{ label: '单独配置', value: 'resourceAction', color: 'orange' },
{ label: '通用配置', value: 'strategy', color: 'default' },
],
} as ISchema,
},
{
type: 'hasMany',
name: 'fields',
@ -100,6 +114,18 @@ export const roleCollectionsSchema: ISchema = {
},
},
column3: {
type: 'void',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
properties: {
usingConfig: {
type: 'string',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
column4: {
type: 'void',
title: 'Actions',
'x-component': 'Table.Column',
@ -136,6 +162,14 @@ export const roleCollectionsSchema: ISchema = {
{ value: false, label: '使用通用权限' },
{ value: true, label: '单独配置权限' },
],
'x-reactions': {
target: 'actions',
fulfill: {
state: {
hidden: '{{!$self.value}}',
},
},
},
},
actions: {
'x-component': 'RolesResourcesActions',

View File

@ -28,6 +28,16 @@ const collection = {
description: '使用英文',
} as ISchema,
},
{
type: 'boolean',
name: 'default',
interface: 'boolean',
uiSchema: {
title: '默认角色',
type: 'boolean',
'x-component': 'Checkbox',
} as ISchema,
},
],
};
@ -44,7 +54,7 @@ export const roleSchema: ISchema = {
resource: 'roles',
action: 'list',
params: {
pageSize: 5,
pageSize: 50,
filter: {},
sort: ['createdAt'],
appends: [],
@ -92,6 +102,10 @@ export const roleSchema: ISchema = {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
default: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
@ -156,6 +170,18 @@ export const roleSchema: ISchema = {
},
},
column3: {
type: 'void',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
properties: {
default: {
type: 'string',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
column4: {
type: 'void',
title: 'Actions',
'x-component': 'Table.Column',
@ -246,6 +272,10 @@ export const roleSchema: ISchema = {
'x-decorator': 'FormItem',
'x-disabled': true,
},
default: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',

View File

@ -10,6 +10,7 @@ export const useRoleResourceValues = (options) => {
resourceOf: record.roleName,
action: 'get',
params: {
appends: ['actions', 'actions.scope'],
filterByTk: record.name,
},
},
@ -17,6 +18,9 @@ export const useRoleResourceValues = (options) => {
);
useEffect(() => {
if (record.usingConfig === 'strategy') {
options.onSuccess({
data: {},
});
return;
}
if (visible) {

View File

@ -1,18 +1,23 @@
import { useForm } from '@formily/react';
import { useAPIClient, useRecord } from '../../../';
import { useActionContext, useAPIClient, useRecord, useResourceActionContext } from '../../../';
export const useSaveRoleResourceAction = () => {
const form = useForm();
const api = useAPIClient();
const record = useRecord();
const ctx = useActionContext();
const { refresh } = useResourceActionContext();
return {
async run() {
await api.resource('roles.resources', record.roleName).create({
await api.resource('roles.resources', record.roleName)[record.exists ? 'update' : 'create']({
filterByTk: record.name,
values: {
...form.values,
name: record.name,
},
});
ctx.setVisible(false);
refresh();
},
};
};

View File

@ -3,7 +3,6 @@ import { observer, useField } from '@formily/react';
import { Button, Modal } from 'antd';
import classnames from 'classnames';
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { SortableItem } from '../../common';
import { useDesigner } from '../../hooks';
@ -114,7 +113,7 @@ Action.Designer = () => {
};
Action.Link = observer((props) => {
return <Action {...props} component={Link} className={'nb-action-link'} />;
return <Action {...props} component={'a'} className={'nb-action-link'} />;
});
Action.Drawer = ActionDrawer;

View File

@ -4,28 +4,34 @@ const availableActions: {
[key: string]: AvailableActionOptions;
} = {
create: {
displayName: '{{ t("Create") }}',
type: 'new-data',
},
import: {
displayName: '{{ t("Import") }}',
displayName: '{{t("Add new")}}',
type: 'new-data',
onNewRecord: true,
allowConfigureFields: true,
},
// import: {
// displayName: '{{t("Import")}}',
// type: 'new-data',
// scope: false,
// },
export: {
displayName: '{{ t("Export") }}',
type: 'new-data',
displayName: '{{t("Export")}}',
type: 'old-data',
allowConfigureFields: true,
},
view: {
displayName: '{{ t("View") }}',
displayName: '{{t("View")}}',
type: 'old-data',
aliases: ['get', 'list'],
allowConfigureFields: true,
},
update: {
displayName: '{{ t("Edit") }}',
displayName: '{{t("Edit")}}',
type: 'old-data',
allowConfigureFields: true,
},
destroy: {
displayName: '{{ t("Delete") }}',
displayName: '{{t("Delete")}}',
type: 'old-data',
},
};