feat: improve acl module
This commit is contained in:
parent
5e51973b21
commit
9704f8a342
@ -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) => {
|
||||
obj[action.name] = 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) => {
|
||||
|
46
packages/client/src/acl/Configuration/ScopeSelect.tsx
Normal file
46
packages/client/src/acl/Configuration/ScopeSelect.tsx
Normal 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>
|
||||
);
|
||||
};
|
202
packages/client/src/acl/Configuration/schemas/scopes.ts
Normal file
202
packages/client/src/acl/Configuration/schemas/scopes.ts
Normal 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',
|
||||
// },
|
||||
// },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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') {
|
||||
return s;
|
||||
if (s['x-component'] === 'RecordPicker.Options') {
|
||||
return s.reduceProperties((buf, s) => {
|
||||
if (s['x-component'] === 'Table.RowSelection') {
|
||||
return s;
|
||||
}
|
||||
return buf;
|
||||
}, null);
|
||||
}
|
||||
return buf;
|
||||
}, new Schema({}));
|
||||
}, null);
|
||||
const [value, setValue] = useState(field.value);
|
||||
const form = useMemo(
|
||||
() =>
|
||||
createForm({
|
||||
initialValues: {
|
||||
[s.name]: field.value,
|
||||
},
|
||||
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();
|
||||
|
@ -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,30 +34,39 @@ const schema: ISchema = {
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
rowSelection: {
|
||||
'x-component': 'RowSelection',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
objectValue: true,
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
dataSource: [
|
||||
{ id: 1, name: 'Name1' },
|
||||
{ id: 2, name: 'Name2' },
|
||||
{ id: 3, name: 'Name3' },
|
||||
],
|
||||
},
|
||||
options: {
|
||||
'x-component': 'RecordPicker.Options',
|
||||
type: 'void',
|
||||
title: 'Drawer Title',
|
||||
properties: {
|
||||
column1: {
|
||||
type: 'void',
|
||||
title: 'Name',
|
||||
'x-component': 'RowSelection.Column',
|
||||
input: {
|
||||
type: 'array',
|
||||
title: `编辑模式`,
|
||||
'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: {
|
||||
name: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
'x-read-pretty': true,
|
||||
column1: {
|
||||
type: 'void',
|
||||
title: 'Name',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -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>
|
||||
);
|
||||
|
24
packages/client/src/schema-component/core/FormProvider.tsx
Normal file
24
packages/client/src/schema-component/core/FormProvider.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -1,5 +1,7 @@
|
||||
export * from './DesignableSwitch';
|
||||
export * from './FormProvider';
|
||||
export * from './RemoteSchemaComponent';
|
||||
export * from './SchemaComponent';
|
||||
export * from './SchemaComponentOptions';
|
||||
export * from './SchemaComponentProvider';
|
||||
|
||||
|
@ -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: [
|
||||
|
@ -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'],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user