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 { 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, Select, Table, Tag } from 'antd'; import { Checkbox, Table, Tag } from 'antd';
import React from 'react'; import React 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 '../..';
import { ScopeSelect } from './ScopeSelect';
const toActionMap = (arr: any[]) => { const toActionMap = (arr: any[]) => {
const obj = {}; const obj = {};
arr?.forEach((action) => { arr?.forEach?.((action) => {
obj[action.name] = action; if (action.name) {
obj[action.name] = action;
}
}); });
return obj; return obj;
}; };
@ -52,11 +55,16 @@ export const RolesResourcesActions = connect((props) => {
} }
onChange(Object.values(actionMap)); onChange(Object.values(actionMap));
}; };
const setScope = (actionName, scope) => {
if (!actionMap[actionName]) {
toggleAction(actionName);
}
actionMap[actionName]['scope'] = scope;
};
const allChecked = {}; const allChecked = {};
for (const action of availableActionsWithFields) { for (const action of availableActionsWithFields) {
allChecked[action.name] = collection?.fields?.length === actionMap?.[action.name]?.fields?.length; allChecked[action.name] = collection?.fields?.length === actionMap?.[action.name]?.fields?.length;
} }
return ( return (
<div> <div>
<FormLayout layout={'vertical'}> <FormLayout layout={'vertical'}>
@ -95,7 +103,15 @@ export const RolesResourcesActions = connect((props) => {
{ {
dataIndex: 'scope', dataIndex: 'scope',
title: '可操作的数据范围', title: '可操作的数据范围',
render: () => <Select size={'small'} />, render: (value, action) =>
!action.onNewRecord && (
<ScopeSelect
value={value}
onChange={(scope) => {
setScope(action.name, scope);
}}
/>
),
}, },
]} ]}
dataSource={availableActions?.map((item) => { 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, FormContext,
mapProps, mapProps,
mapReadPretty, mapReadPretty,
observer,
RecursionField, RecursionField,
Schema,
useField, useField,
useFieldSchema useFieldSchema,
useForm
} from '@formily/react'; } from '@formily/react';
import { toArr } from '@formily/shared'; import { toArr } from '@formily/shared';
import { Button, Drawer, Select, Space, Tag } from 'antd'; import { Button, Drawer, Select, Space, Tag } from 'antd';
import React, { createContext, useContext, useMemo, useState } from 'react'; import React, { createContext, useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAttach } from '../../hooks/useAttach'; 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 [visible, setVisible] = useState(false);
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const field = useField<Field>(); const field = useField<Field>();
const s = fieldSchema.reduceProperties((buf, s) => { const s = fieldSchema.reduceProperties((buf, s) => {
if (s['x-component'] === 'RowSelection') { if (s['x-component'] === 'RecordPicker.Options') {
return s; return s.reduceProperties((buf, s) => {
if (s['x-component'] === 'Table.RowSelection') {
return s;
}
return buf;
}, null);
} }
return buf; return buf;
}, new Schema({})); }, null);
const [value, setValue] = useState(field.value);
const form = useMemo( const form = useMemo(
() => () =>
createForm({ createForm({
initialValues: { initialValues: s?.name
[s.name]: field.value, ? {
}, [s.name]: field.value,
}
: {},
effects() { effects() {
onFormSubmit((form) => { onFormSubmit((form) => {
field.value = form.values[s.name]; setValue(form.values[s.name]);
console.log('field.value', 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: '' })); const f = useAttach(form.createVoidField({ ...field.props, basePath: '' }));
return ( return (
<div> <div>
<Select <Select
size={props.size}
mode={props.mode}
fieldNames={fieldNames}
onClick={() => { onClick={() => {
setVisible(true); setVisible(true);
}} }}
labelInValue={true}
value={toValue(value)}
open={false} open={false}
></Select> ></Select>
<FormContext.Provider value={form}> <FormContext.Provider value={form}>
<FieldContext.Provider value={f}> <FieldContext.Provider value={f}>
<Drawer <ActionContext.Provider value={{ visible, setVisible }}>
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>
}
>
<RecursionField <RecursionField
onlyRenderProperties onlyRenderProperties
basePath={f.address} basePath={f.address}
schema={fieldSchema} schema={fieldSchema}
// filterProperties={(s) => { filterProperties={(s) => {
// return s['x-component'] === 'RowSelection'; return s['x-component'] === 'RecordPicker.Options';
// }} }}
/> />
</Drawer> </ActionContext.Provider>
</FieldContext.Provider> </FieldContext.Provider>
</FormContext.Provider> </FormContext.Provider>
</div> </div>
@ -125,6 +140,44 @@ export const RecordPicker: any = connect(
mapReadPretty(ReadPrettyRecordPicker), 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 = () => { RecordPicker.SelectedItem = () => {
const ctx = useContext(RowContext); const ctx = useContext(RowContext);
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();

View File

@ -3,7 +3,7 @@
*/ */
import { FormItem } from '@formily/antd'; import { FormItem } from '@formily/antd';
import { ISchema } from '@formily/react'; 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'; import React from 'react';
const schema: ISchema = { const schema: ISchema = {
@ -18,6 +18,13 @@ const schema: ISchema = {
], ],
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'RecordPicker', 'x-component': 'RecordPicker',
'x-component-props': {
mode: 'tags',
fieldNames: {
label: 'name',
value: 'id',
},
},
'x-reactions': { 'x-reactions': {
target: 'read', target: 'read',
fulfill: { fulfill: {
@ -27,30 +34,39 @@ const schema: ISchema = {
}, },
}, },
properties: { properties: {
rowSelection: { options: {
'x-component': 'RowSelection', 'x-component': 'RecordPicker.Options',
'x-component-props': { type: 'void',
rowKey: 'id', title: 'Drawer Title',
objectValue: true,
rowSelection: {
type: 'checkbox',
},
dataSource: [
{ id: 1, name: 'Name1' },
{ id: 2, name: 'Name2' },
{ id: 3, name: 'Name3' },
],
},
properties: { properties: {
column1: { input: {
type: 'void', type: 'array',
title: 'Name', title: `编辑模式`,
'x-component': 'RowSelection.Column', 'x-component': 'Table.RowSelection',
'x-component-props': {
rowKey: 'id',
objectValue: true,
rowSelection: {
type: 'checkbox',
},
dataSource: [
{ id: 1, name: 'Name1' },
{ id: 2, name: 'Name2' },
{ id: 3, name: 'Name3' },
],
},
properties: { properties: {
name: { column1: {
type: 'string', type: 'void',
'x-component': 'Input', title: 'Name',
'x-read-pretty': true, 'x-component': 'Table.Column',
properties: {
name: {
type: 'string',
'x-component': 'Input',
'x-read-pretty': true,
},
},
}, },
}, },
}, },
@ -102,7 +118,7 @@ const schema: ISchema = {
export default () => { export default () => {
return ( return (
<SchemaComponentProvider components={{ Input, RecordPicker, RowSelection, FormItem, Action }}> <SchemaComponentProvider components={{ Table, Input, RecordPicker, FormItem, Action }}>
<SchemaComponent schema={schema} /> <SchemaComponent schema={schema} />
</SchemaComponentProvider> </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 './DesignableSwitch';
export * from './FormProvider';
export * from './RemoteSchemaComponent'; export * from './RemoteSchemaComponent';
export * from './SchemaComponent'; export * from './SchemaComponent';
export * from './SchemaComponentOptions'; export * from './SchemaComponentOptions';
export * from './SchemaComponentProvider'; export * from './SchemaComponentProvider';

View File

@ -238,7 +238,10 @@ export class PluginACL extends Plugin {
await this.writeRolesToACL(); 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'); const repository = this.app.db.getRepository('roles');
await repository.createMany({ await repository.createMany({
records: [ records: [

View File

@ -5,22 +5,6 @@ import * as actions from './actions/users';
import * as middlewares from './middlewares'; import * as middlewares from './middlewares';
export default class UsersPlugin extends Plugin { 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() { async beforeLoad() {
this.db.on('users.afterCreateWithAssociations', async (model, options) => { this.db.on('users.afterCreateWithAssociations', async (model, options) => {
@ -83,4 +67,23 @@ export default class UsersPlugin extends Plugin {
directory: resolve(__dirname, 'collections'), 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 Application from './application';
import { Plugin } from './plugin'; import { Plugin } from './plugin';
import { CleanOptions, SyncOptions } from '@nocobase/database';
interface PluginManagerOptions { interface PluginManagerOptions {
app: Application; app: Application;