feat(client): improve acl module
This commit is contained in:
parent
6f2069e918
commit
d1ac62ddaf
@ -3,6 +3,8 @@ export interface AvailableActionOptions {
|
||||
type: 'new-data' | 'old-data';
|
||||
displayName?: string;
|
||||
resource?: string;
|
||||
onNewRecord?: boolean;
|
||||
allowConfigureFields?: boolean;
|
||||
}
|
||||
|
||||
export class AclAvailableAction {
|
||||
|
@ -47,7 +47,7 @@ export const RoleConfigure = () => {
|
||||
'x-component': 'Checkbox',
|
||||
'x-content': '允许配置系统,包括界面配置、数据表配置、权限配置、系统配置等全部配置项',
|
||||
},
|
||||
strategy: {
|
||||
'strategy.actions': {
|
||||
title: '通用数据操作权限',
|
||||
description: '所有数据表都默认使用通用数据操作权限;同时,可以针对每个数据表单独配置权限。',
|
||||
'x-component': 'StrategyActions',
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user