feat: scope select for acl resource actions

This commit is contained in:
chenos 2022-04-08 13:47:47 +08:00
parent 1859879978
commit adea10a4d8
4 changed files with 409 additions and 238 deletions

View File

@ -2,7 +2,7 @@ import { FormItem, FormLayout } from '@formily/antd';
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { connect, useField, useForm } from '@formily/react'; import { connect, useField, useForm } from '@formily/react';
import { Checkbox, Table, Tag } from 'antd'; import { Checkbox, Table, Tag } from 'antd';
import React from 'react'; import React, { createContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAvailableActions } from '.'; import { useAvailableActions } from '.';
import { useCollectionManager, useCompile, useRecord } from '../..'; import { useCollectionManager, useCompile, useRecord } from '../..';
@ -18,6 +18,8 @@ const toActionMap = (arr: any[]) => {
return obj; return obj;
}; };
export const RoleResourceCollectionContext = createContext<any>({});
export const RolesResourcesActions = connect((props) => { export const RolesResourcesActions = connect((props) => {
const { onChange } = props; const { onChange } = props;
const form = useForm(); const form = useForm();
@ -67,128 +69,130 @@ export const RolesResourcesActions = connect((props) => {
} }
return ( return (
<div> <div>
<FormLayout layout={'vertical'}> <RoleResourceCollectionContext.Provider value={collection}>
<FormItem label={'操作权限'}> <FormLayout layout={'vertical'}>
<Table <FormItem label={'操作权限'}>
size={'small'} <Table
pagination={false} size={'small'}
columns={[ pagination={false}
{ columns={[
dataIndex: 'displayName', {
title: '操作', dataIndex: 'displayName',
render: (value) => compile(value), title: '操作',
}, render: (value) => compile(value),
{ },
dataIndex: 'onNewRecord', {
title: '类型', dataIndex: 'onNewRecord',
render: (onNewRecord) => title: '类型',
onNewRecord ? ( render: (onNewRecord) =>
<Tag color={'green'}>{t('Operate on new record')}</Tag> onNewRecord ? (
) : ( <Tag color={'green'}>{t('Operate on new record')}</Tag>
<Tag color={'geekblue'}>{t('Operate on existing record')}</Tag> ) : (
), <Tag color={'geekblue'}>{t('Operate on existing record')}</Tag>
}, ),
{ },
dataIndex: 'enabled', {
title: '允许操作', dataIndex: 'enabled',
render: (enabled, action) => ( title: '允许操作',
<Checkbox render: (enabled, action) => (
checked={enabled} <Checkbox
onChange={() => { checked={enabled}
toggleAction(action.name); onChange={() => {
}} toggleAction(action.name);
/>
),
},
{
dataIndex: 'scope',
title: '可操作的数据范围',
render: (value, action) =>
!action.onNewRecord && (
<ScopeSelect
value={value}
onChange={(scope) => {
setScope(action.name, scope);
}} }}
/> />
), ),
}, },
]} {
dataSource={availableActions?.map((item) => { dataIndex: 'scope',
let enabled = false; title: '可操作的数据范围',
let scope = null; render: (value, action) =>
if (actionMap[item.name]) { !action.onNewRecord && (
enabled = true; <ScopeSelect
scope = actionMap[item.name]['scope']; value={value}
} onChange={(scope) => {
return { setScope(action.name, scope);
...item, }}
enabled, />
scope, ),
}; },
})} ]}
/> dataSource={availableActions?.map((item) => {
</FormItem> let enabled = false;
<FormItem label={'字段权限'}> let scope = null;
<Table if (actionMap[item.name]) {
pagination={false} enabled = true;
dataSource={fieldPermissions} scope = actionMap[item.name]['scope'];
columns={[ }
{
dataIndex: ['uiSchema', 'title'],
title: '字段名称',
render: (value) => compile(value),
},
...availableActionsWithFields.map((action) => {
const checked = allChecked?.[action.name];
return { return {
dataIndex: action.name, ...item,
title: ( enabled,
<> scope,
};
})}
/>
</FormItem>
<FormItem label={'字段权限'}>
<Table
pagination={false}
dataSource={fieldPermissions}
columns={[
{
dataIndex: ['uiSchema', 'title'],
title: '字段名称',
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 = collection?.fields?.map?.((item) => item.name);
}
actionMap[action.name] = item;
onChange(Object.values(actionMap));
}}
/>{' '}
{compile(action.displayName)}
</>
),
render: (checked, field) => (
<Checkbox <Checkbox
checked={checked} checked={checked}
onChange={() => { onChange={() => {
const item = actionMap[action.name] || { const item = actionMap[action.name] || {
name: action.name, name: action.name,
}; };
const fields: string[] = item.fields || [];
if (checked) { if (checked) {
item.fields = []; const index = fields.indexOf(field.name);
fields.splice(index, 1);
} else { } else {
item.fields = collection?.fields?.map?.((item) => item.name); fields.push(field.name);
} }
item.fields = fields;
actionMap[action.name] = item; actionMap[action.name] = item;
onChange(Object.values(actionMap)); onChange(Object.values(actionMap));
}} }}
/>{' '} />
{compile(action.displayName)} ),
</> };
), }),
render: (checked, field) => ( ]}
<Checkbox />
checked={checked} </FormItem>
onChange={() => { </FormLayout>
const item = actionMap[action.name] || { </RoleResourceCollectionContext.Provider>
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> </div>
); );
}); });

View File

@ -1,14 +1,15 @@
import { ISchema, useForm } from '@formily/react'; import { ISchema } from '@formily/react';
import { useActionContext } from '../../../'; import { useContext, useEffect } from 'react';
import { useAPIClient, useRequest } from '../../../api-client'; import { useFormBlockContext } from '../../../block-provider';
import { useRecord } from '../../../record-provider'; import { useFilterOptions } from '../../../schema-component';
import { RoleResourceCollectionContext } from '../RolesResourcesActions';
const collection = { export const rolesResourcesScopesCollection = {
name: 'rolesResourcesScopes', name: 'rolesResourcesScopes',
fields: [ fields: [
{ {
type: 'string', type: 'string',
name: 'title', name: 'name',
interface: 'input', interface: 'input',
uiSchema: { uiSchema: {
title: '名称', title: '名称',
@ -20,6 +21,19 @@ const collection = {
], ],
}; };
const useFormBlockProps = () => {
const { name } = useContext(RoleResourceCollectionContext);
const ctx = useFormBlockContext();
useEffect(() => {
ctx.form.setInitialValues({
resourceName: name,
});
}, [name]);
return {
form: ctx.form,
};
};
export const scopesSchema: ISchema = { export const scopesSchema: ISchema = {
type: 'object', type: 'object',
properties: { properties: {
@ -31,88 +45,279 @@ export const scopesSchema: ISchema = {
label: 'name', label: 'name',
value: 'id', value: 'id',
}, },
association: {
target: 'rolesResourcesScopes',
},
onChange: '{{ onChange }}', onChange: '{{ onChange }}',
}, },
properties: { properties: {
options: { selector: {
'x-decorator': 'RolesResourcesScopesSelectedRowKeysProvider',
'x-component': 'RecordPicker.Options',
type: 'void', type: 'void',
title: '可操作的数据范围', title: '{{ t("Select record") }}',
'x-component': 'RecordPicker.Selector',
'x-component-props': {
className: 'nb-record-picker-selector',
},
properties: { properties: {
actions: { tableBlock: {
type: 'void', type: 'void',
'x-component': 'ActionBar', 'x-decorator': 'TableSelectorProvider',
'x-component-props': { 'x-decorator-props': {
style: { collection: rolesResourcesScopesCollection,
marginBottom: 16, resource: 'rolesResourcesScopes',
action: 'list',
params: {
pageSize: 20,
}, },
useParams() {
const ctx = useContext(RoleResourceCollectionContext);
return {
filter: {
$or: [{ resourceName: ctx.name }, { resourceName: null }],
},
};
},
rowKey: 'id',
}, },
'x-component': 'BlockItem',
properties: { properties: {
// delete: { actions: {
// type: 'void',
// title: '删除',
// 'x-component': 'Action',
// },
create: {
type: 'void', type: 'void',
title: '添加数据范围', 'x-component': 'ActionBar',
'x-component': 'Action',
'x-component-props': { 'x-component-props': {
type: 'primary', style: {
marginBottom: 16,
},
}, },
properties: { properties: {
drawer: { create: {
type: 'void', type: 'void',
'x-component': 'Action.Drawer', title: '{{ t("Add new") }}',
'x-decorator': 'Form', 'x-action': 'create',
title: '添加数据范围', 'x-component': 'Action',
'x-component-props': {
icon: 'PlusOutlined',
openMode: 'drawer',
type: 'primary',
},
properties: { properties: {
name: { drawer: {
title: '数据范围名称',
'x-component': 'Input',
'x-decorator': 'FormItem',
},
// scope: {
// 'x-component': 'Input',
// 'x-decorator': 'FormItem',
// },
footer: {
type: 'void', type: 'void',
'x-component': 'Action.Drawer.Footer', title: '{{ t("Add record") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
properties: { properties: {
action1: { formBlock: {
title: 'Cancel', type: 'void',
'x-component': 'Action', 'x-decorator': 'FormBlockProvider',
'x-component-props': { 'x-decorator-props': {
useAction: '{{ cm.useCancelAction }}', resource: 'rolesResourcesScopes',
collection: rolesResourcesScopesCollection,
// useParams: '{{ useParamsFromRecord }}',
},
'x-component': 'CardItem',
properties: {
form: {
type: 'void',
'x-component': 'FormV2',
'x-component-props': {
useProps: useFormBlockProps,
},
properties: {
name: {
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
scope: {
type: 'object',
title: 'Data scope',
name: 'filter',
'x-decorator': 'FormItem',
'x-component': 'Filter',
'x-component-props': {
useProps() {
const ctx = useContext(RoleResourceCollectionContext);
const options = useFilterOptions(ctx.name);
console.log('ctx.name', ctx.name, options);
return {
options,
};
},
},
},
actions: {
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
marginTop: 24,
},
},
properties: {
create: {
title: '{{ t("Submit") }}',
'x-action': 'submit',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
htmlType: 'submit',
useProps: '{{ useCreateActionProps }}',
},
},
},
},
},
},
}, },
}, },
action2: { },
title: 'Submit', },
'x-component': 'Action', },
},
},
},
value: {
type: 'array',
'x-component': 'TableV2.Selector',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
useProps: '{{ useTableSelectorProps }}',
},
properties: {
column1: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
properties: {
name: {
type: 'string',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
actions: {
type: 'void',
title: '{{ t("Actions") }}',
'x-action-column': 'actions',
'x-component': 'TableV2.Column',
properties: {
actions: {
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
properties: {
edit: {
type: 'void',
title: '{{ t("Edit") }}',
'x-action': 'update',
'x-component': 'Action.Link',
'x-component-props': { 'x-component-props': {
type: 'primary', openMode: 'drawer',
useAction() { icon: 'EditOutlined',
const api = useAPIClient(); },
const ctx = useActionContext(); properties: {
const form = useForm(); drawer: {
const record = useRecord(); type: 'void',
return { title: '{{ t("Edit record") }}',
async run() { 'x-component': 'Action.Container',
await api.resource('rolesResourcesScopes').create({ 'x-component-props': {
values: { className: 'nb-action-popup',
...form.values, },
resourceName: record.name, properties: {
formBlock: {
type: 'void',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
resource: 'rolesResourcesScopes',
collection: rolesResourcesScopesCollection,
action: 'get',
useParams: '{{ useParamsFromRecord }}',
},
'x-component': 'CardItem',
properties: {
form: {
type: 'void',
'x-component': 'FormV2',
'x-component-props': {
useProps: '{{ useFormBlockProps }}',
},
properties: {
name: {
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
scope: {
type: 'object',
title: 'Data scope',
name: 'filter',
'x-decorator': 'FormItem',
'x-component': 'Filter',
'x-component-props': {
useProps() {
const ctx = useContext(RoleResourceCollectionContext);
const options = useFilterOptions(ctx.name);
return {
options,
};
},
},
},
actions: {
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
marginTop: 24,
},
},
properties: {
update: {
title: '{{ t("Submit") }}',
'x-action': 'submit',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
htmlType: 'submit',
useProps: '{{ useUpdateActionProps }}',
},
},
},
},
},
}, },
}); },
ctx.setVisible(false);
api.service('rolesResourcesScopesList')?.refresh?.();
}, },
}; },
}, },
}, },
}, },
destroy: {
title: '{{ t("Delete") }}',
'x-action': 'destroy',
'x-component': 'Action.Link',
'x-designer': 'Action.Designer',
'x-component-props': {
icon: 'DeleteOutlined',
confirm: {
title: "{{t('Delete record')}}",
content: "{{t('Are you sure you want to delete it?')}}",
},
useProps: '{{ useDestroyActionProps }}',
},
},
}, },
}, },
}, },
@ -121,76 +326,28 @@ export const scopesSchema: ISchema = {
}, },
}, },
}, },
input: { footer: {
type: 'array', 'x-component': 'Action.Container.Footer',
'x-component': 'Table.RowSelection', 'x-component-props': {},
'x-component-props': { properties: {
rowKey: 'id', actions: {
objectValue: true, type: 'void',
rowSelection: { 'x-component': 'ActionBar',
type: 'radio', 'x-component-props': {},
}, properties: {
// useSelectedRowKeys() { submit: {
// const [selectedRowKeys, setSelectedRowKeys] = useRolesResourcesScopesSelectedRowKeys(); title: '{{ t("Submit") }}',
// return [selectedRowKeys, setSelectedRowKeys]; 'x-action': 'submit',
// }, 'x-component': 'Action',
useDataSource(options) { 'x-designer': 'Action.Designer',
const record = useRecord(); 'x-component-props': {
return useRequest( type: 'primary',
{ htmlType: 'submit',
resource: 'rolesResourcesScopes', useProps: '{{ usePickActionProps }}',
action: 'list',
params: {
sort: 'id',
filter: JSON.stringify({
$or: [
{
'resourceName.$eq': record.name,
},
{
'resourceName.$eq': '*',
},
],
}),
}, },
}, },
{
...options,
uid: 'rolesResourcesScopesList',
},
);
},
// dataSource: [
// { id: 1, name: 'Name1' },
// { id: 2, name: 'Name2' },
// { id: 3, name: 'Name3' },
// ],
},
properties: {
column1: {
type: 'void',
title: 'Name',
'x-component': 'Table.Column',
properties: {
name: {
type: 'string',
'x-component': 'Input',
'x-read-pretty': true,
},
}, },
}, },
column2: {
type: 'void',
title: 'Actions',
'x-component': 'Table.Column',
// properties: {
// delete: {
// type: 'void',
// title: '删除',
// 'x-component': 'Action.Link',
// },
// },
},
}, },
}, },
}, },

View File

@ -49,6 +49,7 @@ export const FilterGroup = connect((props) => {
<Trans> <Trans>
{'Meet '} {'Meet '}
<Select <Select
style={{ width: 'auto' }}
value={logic} value={logic}
onChange={(value) => { onChange={(value) => {
setLogic(value); setLogic(value);

View File

@ -41,13 +41,22 @@ const usePickActionProps = () => {
}; };
}; };
const useAssociation = (props) => {
const fieldSchema = useFieldSchema();
const { association } = props;
const { getField } = useCollection();
if (association) {
return association;
}
return getField(fieldSchema.name);
}
export const InputRecordPicker: React.FC<any> = (props) => { export const InputRecordPicker: React.FC<any> = (props) => {
const { value, multiple, onChange } = props; const { value, multiple, onChange } = props;
const fieldNames = useFieldNames(props); const fieldNames = useFieldNames(props);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { getField } = useCollection(); const collectionField = useAssociation(props);
const collectionField = getField(fieldSchema.name);
const options = Array.isArray(value) ? value : value ? [value] : []; const options = Array.isArray(value) ? value : value ? [value] : [];
const [selectedRows, setSelectedRows] = useState(options); const [selectedRows, setSelectedRows] = useState(options);
const values = options?.map((option) => option[fieldNames.value]); const values = options?.map((option) => option[fieldNames.value]);