feat: improve acl module

This commit is contained in:
chenos 2022-02-28 22:10:04 +08:00
parent 5e51973b21
commit 9704f8a342
10 changed files with 448 additions and 83 deletions

View File

@ -1,16 +1,19 @@
import { FormItem, FormLayout } from '@formily/antd';
import { ArrayField } from '@formily/core';
import { connect, useField, useForm } from '@formily/react';
import { Checkbox, Select, Table, Tag } from 'antd';
import { Checkbox, Table, Tag } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useAvailableActions } from '.';
import { useCollectionManager, useCompile, useRecord } from '../..';
import { ScopeSelect } from './ScopeSelect';
const toActionMap = (arr: any[]) => {
const obj = {};
arr?.forEach((action) => {
arr?.forEach?.((action) => {
if (action.name) {
obj[action.name] = action;
}
});
return obj;
};
@ -52,11 +55,16 @@ export const RolesResourcesActions = connect((props) => {
}
onChange(Object.values(actionMap));
};
const setScope = (actionName, scope) => {
if (!actionMap[actionName]) {
toggleAction(actionName);
}
actionMap[actionName]['scope'] = scope;
};
const allChecked = {};
for (const action of availableActionsWithFields) {
allChecked[action.name] = collection?.fields?.length === actionMap?.[action.name]?.fields?.length;
}
return (
<div>
<FormLayout layout={'vertical'}>
@ -95,7 +103,15 @@ export const RolesResourcesActions = connect((props) => {
{
dataIndex: 'scope',
title: '可操作的数据范围',
render: () => <Select size={'small'} />,
render: (value, action) =>
!action.onNewRecord && (
<ScopeSelect
value={value}
onChange={(scope) => {
setScope(action.name, scope);
}}
/>
),
},
]}
dataSource={availableActions?.map((item) => {

View File

@ -0,0 +1,46 @@
import { createForm } from '@formily/core';
import React, { createContext, useContext, useMemo, useState } from 'react';
import { FormProvider, SchemaComponent } from '../../schema-component';
import { scopesSchema } from './schemas/scopes';
const RolesResourcesScopesSelectedRowKeysContext = createContext(null);
const RolesResourcesScopesSelectedRowKeysProvider: React.FC = (props) => {
const [keys, setKeys] = useState([]);
return (
<RolesResourcesScopesSelectedRowKeysContext.Provider value={[keys, setKeys]}>
{props.children}
</RolesResourcesScopesSelectedRowKeysContext.Provider>
);
};
export const useRolesResourcesScopesSelectedRowKeys = () => {
return useContext(RolesResourcesScopesSelectedRowKeysContext);
};
export const ScopeSelect = (props) => {
const form = useMemo(
() =>
createForm({
values: {
scope: props.value,
},
}),
[],
);
console.log('props.value', props.value, form.values);
return (
<FormProvider form={form}>
<SchemaComponent
components={{ RolesResourcesScopesSelectedRowKeysProvider }}
scope={{
onChange(value) {
props?.onChange?.(value);
console.log('onChange', value);
},
}}
schema={scopesSchema}
/>
</FormProvider>
);
};

View File

@ -0,0 +1,202 @@
import { ISchema, useForm } from '@formily/react';
import { useActionContext } from '../../../';
import { useAPIClient, useRequest } from '../../../api-client';
import { useRecord } from '../../../record-provider';
const collection = {
name: 'rolesResourcesScopes',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
title: '名称',
type: 'string',
'x-component': 'Input',
required: true,
} as ISchema,
},
],
};
export const scopesSchema: ISchema = {
type: 'object',
properties: {
scope: {
'x-component': 'RecordPicker',
'x-component-props': {
size: 'small',
fieldNames: {
label: 'name',
value: 'id',
},
onChange: '{{ onChange }}',
},
properties: {
options: {
'x-decorator': 'RolesResourcesScopesSelectedRowKeysProvider',
'x-component': 'RecordPicker.Options',
type: 'void',
title: '可操作的数据范围',
properties: {
actions: {
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 16,
},
},
properties: {
// delete: {
// type: 'void',
// title: '删除',
// 'x-component': 'Action',
// },
create: {
type: 'void',
title: '添加数据范围',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
},
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'Form',
title: '添加数据范围',
properties: {
name: {
title: '数据范围名称',
'x-component': 'Input',
'x-decorator': 'FormItem',
},
// scope: {
// 'x-component': 'Input',
// 'x-decorator': 'FormItem',
// },
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
action1: {
title: 'Cancel',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ cm.useCancelAction }}',
},
},
action2: {
title: 'Submit',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction() {
const api = useAPIClient();
const ctx = useActionContext();
const form = useForm();
const record = useRecord();
return {
async run() {
await api.resource('rolesResourcesScopes').create({
values: {
...form.values,
resourceName: record.name,
},
});
ctx.setVisible(false);
api.service('rolesResourcesScopesList')?.refresh?.();
},
};
},
},
},
},
},
},
},
},
},
},
},
input: {
type: 'array',
'x-component': 'Table.RowSelection',
'x-component-props': {
rowKey: 'id',
objectValue: true,
rowSelection: {
type: 'radio',
},
// useSelectedRowKeys() {
// const [selectedRowKeys, setSelectedRowKeys] = useRolesResourcesScopesSelectedRowKeys();
// return [selectedRowKeys, setSelectedRowKeys];
// },
useDataSource(options) {
const record = useRecord();
console.log('useRecord', record);
return useRequest(
{
resource: 'rolesResourcesScopes',
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

@ -6,82 +6,97 @@ import {
FormContext,
mapProps,
mapReadPretty,
observer,
RecursionField,
Schema,
useField,
useFieldSchema
useFieldSchema,
useForm
} from '@formily/react';
import { toArr } from '@formily/shared';
import { Button, Drawer, Select, Space, Tag } from 'antd';
import React, { createContext, useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAttach } from '../../hooks/useAttach';
import { ActionContext } from '../action';
import { ActionContext, useActionContext } from '../action';
const InputRecordPicker: React.FC = (props) => {
const InputRecordPicker: React.FC<any> = (props) => {
const { onChange } = props;
const fieldNames = { label: 'label', value: 'value', ...props.fieldNames };
const [visible, setVisible] = useState(false);
const fieldSchema = useFieldSchema();
const field = useField<Field>();
const s = fieldSchema.reduceProperties((buf, s) => {
if (s['x-component'] === 'RowSelection') {
if (s['x-component'] === 'RecordPicker.Options') {
return s.reduceProperties((buf, s) => {
if (s['x-component'] === 'Table.RowSelection') {
return s;
}
return buf;
}, new Schema({}));
}, null);
}
return buf;
}, null);
const [value, setValue] = useState(field.value);
const form = useMemo(
() =>
createForm({
initialValues: {
initialValues: s?.name
? {
[s.name]: field.value,
},
}
: {},
effects() {
onFormSubmit((form) => {
field.value = form.values[s.name];
console.log('field.value', form.values[s.name]);
setValue(form.values[s.name]);
onChange?.(form.values[s.name]);
});
},
}),
[],
);
const toValue = (value) => {
if (!value) {
return;
}
if (Array.isArray(value)) {
return value.map((item) => {
return {
label: item[fieldNames.label] || item[fieldNames.value],
value: item[fieldNames.value],
};
});
}
return {
label: value[fieldNames.label] || value[fieldNames.value],
value: value[fieldNames.value],
};
};
const f = useAttach(form.createVoidField({ ...field.props, basePath: '' }));
return (
<div>
<Select
size={props.size}
mode={props.mode}
fieldNames={fieldNames}
onClick={() => {
setVisible(true);
}}
labelInValue={true}
value={toValue(value)}
open={false}
></Select>
<FormContext.Provider value={form}>
<FieldContext.Provider value={f}>
<Drawer
width={'50%'}
placement={'right'}
destroyOnClose
visible={visible}
onClose={() => setVisible(false)}
footer={
<Space style={{ justifyContent: 'flex-end', width: '100%' }}>
<Button
type={'primary'}
onClick={async () => {
await form.submit();
setVisible(false);
}}
>
Submit
</Button>
</Space>
}
>
<ActionContext.Provider value={{ visible, setVisible }}>
<RecursionField
onlyRenderProperties
basePath={f.address}
schema={fieldSchema}
// filterProperties={(s) => {
// return s['x-component'] === 'RowSelection';
// }}
filterProperties={(s) => {
return s['x-component'] === 'RecordPicker.Options';
}}
/>
</Drawer>
</ActionContext.Provider>
</FieldContext.Provider>
</FormContext.Provider>
</div>
@ -125,6 +140,44 @@ export const RecordPicker: any = connect(
mapReadPretty(ReadPrettyRecordPicker),
);
RecordPicker.Options = observer((props) => {
const field = useField();
const form = useForm();
const { visible, setVisible } = useActionContext();
const { t } = useTranslation();
return (
<Drawer
width={'50%'}
placement={'right'}
title={field.title}
{...props}
destroyOnClose
visible={visible}
onClose={() => setVisible(false)}
footer={
<Space style={{ justifyContent: 'flex-end', width: '100%' }}>
<Button
onClick={async () => {
setVisible(false);
}}
>
{t('Cancel')}
</Button>
<Button
type={'primary'}
onClick={async () => {
await form.submit();
setVisible(false);
}}
>
{t('Submit')}
</Button>
</Space>
}
/>
);
});
RecordPicker.SelectedItem = () => {
const ctx = useContext(RowContext);
const fieldSchema = useFieldSchema();

View File

@ -3,7 +3,7 @@
*/
import { FormItem } from '@formily/antd';
import { ISchema } from '@formily/react';
import { Action, Input, RecordPicker, RowSelection, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import { Action, Input, RecordPicker, SchemaComponent, SchemaComponentProvider, Table } from '@nocobase/client';
import React from 'react';
const schema: ISchema = {
@ -18,6 +18,13 @@ const schema: ISchema = {
],
'x-decorator': 'FormItem',
'x-component': 'RecordPicker',
'x-component-props': {
mode: 'tags',
fieldNames: {
label: 'name',
value: 'id',
},
},
'x-reactions': {
target: 'read',
fulfill: {
@ -27,8 +34,15 @@ const schema: ISchema = {
},
},
properties: {
rowSelection: {
'x-component': 'RowSelection',
options: {
'x-component': 'RecordPicker.Options',
type: 'void',
title: 'Drawer Title',
properties: {
input: {
type: 'array',
title: `编辑模式`,
'x-component': 'Table.RowSelection',
'x-component-props': {
rowKey: 'id',
objectValue: true,
@ -45,7 +59,7 @@ const schema: ISchema = {
column1: {
type: 'void',
title: 'Name',
'x-component': 'RowSelection.Column',
'x-component': 'Table.Column',
properties: {
name: {
type: 'string',
@ -58,6 +72,8 @@ const schema: ISchema = {
},
},
},
},
},
read: {
type: 'array',
title: `阅读模式`,
@ -102,7 +118,7 @@ const schema: ISchema = {
export default () => {
return (
<SchemaComponentProvider components={{ Input, RecordPicker, RowSelection, FormItem, Action }}>
<SchemaComponentProvider components={{ Table, Input, RecordPicker, FormItem, Action }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);

View File

@ -0,0 +1,24 @@
import {
FormProvider as FormilyFormProvider,
IProviderProps,
SchemaExpressionScopeContext,
SchemaOptionsContext
} from '@formily/react';
import React, { useContext } from 'react';
import { SchemaComponentOptions } from './SchemaComponentOptions';
export const FormProvider: React.FC<IProviderProps> = (props) => {
const { children, ...others } = props;
let options = useContext(SchemaOptionsContext);
const expressionScope = useContext(SchemaExpressionScopeContext);
const scope = { ...options?.scope, ...expressionScope };
const components = { ...options?.components };
return (
<FormilyFormProvider {...others}>
<SchemaComponentOptions components={components} scope={scope}>
{children}
</SchemaComponentOptions>
</FormilyFormProvider>
);
};

View File

@ -1,5 +1,7 @@
export * from './DesignableSwitch';
export * from './FormProvider';
export * from './RemoteSchemaComponent';
export * from './SchemaComponent';
export * from './SchemaComponentOptions';
export * from './SchemaComponentProvider';

View File

@ -238,7 +238,10 @@ export class PluginACL extends Plugin {
await this.writeRolesToACL();
});
this.app.on('afterInstallUsersPlugin', async () => {
this.app.on('beforeInstallPlugin', async (plugin) => {
if (plugin.constructor.name !== 'UsersPlugin') {
return;
}
const repository = this.app.db.getRepository('roles');
await repository.createMany({
records: [

View File

@ -5,22 +5,6 @@ import * as actions from './actions/users';
import * as middlewares from './middlewares';
export default class UsersPlugin extends Plugin {
async install() {
const {
adminNickname = 'Super Admin',
adminEmail = 'admin@nocobase.com',
adminPassword = 'admin123',
} = this.options;
const User = this.db.getCollection('users');
await User.repository.create({
values: {
nickname: adminNickname,
email: adminEmail,
password: adminPassword,
},
});
}
async beforeLoad() {
this.db.on('users.afterCreateWithAssociations', async (model, options) => {
@ -83,4 +67,23 @@ export default class UsersPlugin extends Plugin {
directory: resolve(__dirname, 'collections'),
});
}
async install() {
const {
adminNickname = 'Super Admin',
adminEmail = 'admin@nocobase.com',
adminPassword = 'admin123',
} = this.options;
const User = this.db.getCollection('users');
await User.repository.create({
values: {
nickname: adminNickname,
email: adminEmail,
password: adminPassword,
roles: ['admin'],
},
});
}
}

View File

@ -1,6 +1,6 @@
import { CleanOptions, SyncOptions } from '@nocobase/database';
import Application from './application';
import { Plugin } from './plugin';
import { CleanOptions, SyncOptions } from '@nocobase/database';
interface PluginManagerOptions {
app: Application;