feat(acl): supports 'current role' variable and collections filtering (#3181)

* feat(acl): support current role variable

* feat(acl): support to filter collections

* fix: the save conditions button should not be shown

* feat(formDataTemplates): supports to use variables for data scope

* test: make testing more stable

* fix: should only support filtering title and name

* fix: fix parse

* test: make testing more stable

* fix: should not use async function
This commit is contained in:
Zeke Zhang 2023-12-20 12:52:54 +08:00 committed by GitHub
parent 2f892f00d7
commit aaee46e0c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 46 additions and 101 deletions

View File

@ -265,6 +265,7 @@ export class ACL extends EventEmitter {
state,
},
$user: async () => state.currentUser,
$nRole: () => state.currentRole,
},
});
json.filter = filter;

View File

@ -1,88 +0,0 @@
import React from 'react';
import { useCompile } from '../../schema-component';
import { useGetFilterOptions } from '../../schema-component/antd/filter';
import { useValues } from '../../schema-component/antd/filter/useValues';
import { Variable } from '../../schema-component/antd/variable';
interface GetOptionsParams {
schema: any;
operator: string;
maxDepth: number;
getFilterOptions: (collectionName: string) => any[];
count?: number;
}
const getOptions = (
collectionName: string,
{ schema, operator, maxDepth, getFilterOptions, count = 1 }: GetOptionsParams,
) => {
if (count > maxDepth) {
return [];
}
const result = getFilterOptions(collectionName).map((option) => {
if ((option.type !== 'belongsTo' && option.type !== 'hasOne') || !option.target) {
return {
key: option.name,
value: option.name,
label: option.title,
// TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化
disabled: schema?.['x-component'] !== option.schema?.['x-component'],
};
}
const children =
getOptions(option.target, {
schema,
operator,
maxDepth,
count: count + 1,
getFilterOptions,
}) || [];
return {
key: option.name,
value: option.name,
label: option.title,
children,
disabled: children.every((child) => child.disabled),
};
});
return result;
};
const useUserVariable = ({ schema, operator }) => {
const getFilterOptions = useGetFilterOptions();
const options = getOptions('users', { schema, operator, maxDepth: 1, getFilterOptions }) || [];
return {
label: `{{t("Current user")}}`,
value: '$user',
key: '$user',
disabled: options.every((option) => option.disabled),
children: options,
};
};
const useVariableOptions = () => {
const { operator, schema } = useValues();
const userVariable = useUserVariable({ schema, operator });
if (!operator || !schema) return [];
return [userVariable];
};
export function FilterDynamicComponent(props) {
const { value, onChange, renderSchemaComponent } = props;
const options = useVariableOptions();
const compile = useCompile();
const scope = compile(options);
return (
<Variable.Input value={value} onChange={onChange} scope={scope}>
{renderSchemaComponent()}
</Variable.Input>
);
}

View File

@ -1,7 +1,7 @@
import { Spin } from 'antd';
import React, { createContext, useContext } from 'react';
import { useRequest } from '../../api-client';
import { SchemaComponent } from '../../schema-component';
import { SchemaComponent, SchemaComponentContext } from '../../schema-component';
import { MenuItemsProvider } from '../Configuration/MenuItemsProvider';
import { PermissionProvider, SettingCenterPermissionProvider } from '../Configuration/PermisionProvider';
import { roleSchema } from './schemas/roles';
@ -28,12 +28,14 @@ export const useAvailableActions = () => {
export const RoleTable = () => {
return (
<div>
<SchemaComponentContext.Provider value={{ designable: false }}>
<AvailableActionsProver>
<SchemaComponent
schema={roleSchema}
components={{ MenuItemsProvider, SettingCenterPermissionProvider, PermissionProvider }}
/>
</AvailableActionsProver>
</SchemaComponentContext.Provider>
</div>
);
};

View File

@ -1,4 +1,5 @@
import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
import { useRoleResourceValues } from './useRoleResourceValues';
import { useSaveRoleResourceAction } from './useSaveRoleResourceAction';
@ -90,6 +91,31 @@ export const roleCollectionsSchema: ISchema = {
},
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 16,
},
},
properties: {
filter: {
type: 'void',
title: '{{ t("Filter") }}',
default: {
$and: [{ title: { $includes: '' } }, { name: { $includes: '' } }],
},
'x-action': 'filter',
'x-component': 'Filter.Action',
'x-component-props': {
icon: 'FilterOutlined',
useProps: '{{ cm.useFilterActionProps }}',
},
'x-align': 'left',
},
},
},
table1: {
type: 'void',
'x-uid': 'input',

View File

@ -2,7 +2,7 @@ import { ISchema } from '@formily/react';
import { useContext, useEffect } from 'react';
import { useFormBlockContext } from '../../../block-provider';
import { useFilterOptions } from '../../../schema-component';
import { FilterDynamicComponent } from '../FilterDynamicComponent';
import { VariableInput } from '../../../schema-settings';
import { RoleResourceCollectionContext } from '../RolesResourcesActions';
export const rolesResourcesScopesCollection = {
@ -140,11 +140,10 @@ export const scopesSchema: ISchema = {
'x-decorator': 'FormItem',
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: FilterDynamicComponent,
dynamicComponent: VariableInput,
useProps() {
const ctx = useContext(RoleResourceCollectionContext);
const options = useFilterOptions(ctx.name);
console.log('ctx.name', ctx.name, options);
return {
options,
};
@ -269,7 +268,7 @@ export const scopesSchema: ISchema = {
'x-decorator': 'FormItem',
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: FilterDynamicComponent,
dynamicComponent: VariableInput,
useProps() {
const ctx = useContext(RoleResourceCollectionContext);
const options = useFilterOptions(ctx.name);

View File

@ -78,7 +78,7 @@ const AssociationResourceActionProvider = (props) => {
);
const resource = api.resource(request.resource, resourceOf);
return (
<ResourceContext.Provider value={{ type: 'association', resource, association }}>
<ResourceContext.Provider value={{ type: 'association', resource, association, collection }}>
<ResourceActionContext.Provider value={{ ...service, defaultRequest: request, dragSort }}>
<CollectionProvider collection={collection}>{props.children}</CollectionProvider>
</ResourceActionContext.Provider>

View File

@ -579,7 +579,8 @@ export const useFilterActionProps = () => {
const options = useFilterFieldOptions(collection.fields);
const service = useResourceActionContext();
return useFilterFieldProps({
options,
// 目前仅需要支持筛选 title 和 name其它字段可能会报错。详见https://nocobase.height.app/T-2745
options: options.filter((option) => ['title', 'name'].includes(option.name)),
params: service.state?.params?.[0] || service.params,
service,
});

View File

@ -9,6 +9,7 @@ import { mergeFilter } from '../../block-provider';
import { useCollectionManager } from '../../collection-manager';
import { SchemaComponent, SchemaComponentContext, removeNullCondition } from '../../schema-component';
import { ITemplate } from '../../schema-component/antd/form-v2/Templates';
import { VariableInput } from '../VariableInput';
import { AsDefaultTemplate } from './components/AsDefaultTemplate';
import { ArrayCollapse } from './components/DataTemplateTitle';
import { getSelectedIdFilter } from './components/Designer';
@ -143,6 +144,9 @@ export const FormDataTemplates = observer(
title: '{{ t("Assign data scope for the template") }}',
'x-decorator': 'FormItem',
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: VariableInput,
},
'x-decorator-props': {
style: {
marginBottom: '0px',