feat: data scope support variables for association fields (#2049)

* feat: association field support data scope

* refactor: association data scope

* refactor: association data scope local

* refactor: association data scope

* refactor: association data scope  code improve

* refactor: code improve

* fix: useFormVariable

* fix: useFormVariable

* chore: useFormVariable

* chore: useFormVariable

* chore: useFormVariable

* chore: useFormVariable

* refactor: locale improve

* refactor: locale improve

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
katherinehhh 2023-06-22 20:19:34 +08:00 committed by GitHub
parent 6eed9ac2bb
commit cb52b80cf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 338 additions and 22 deletions

View File

@ -703,5 +703,7 @@ export default {
"First or create":"First or create", "First or create":"First or create",
"Update or create":"Update or create", "Update or create":"Update or create",
"Find by the following fields":"Find by the following fields", "Find by the following fields":"Find by the following fields",
"Create":"Create" "Create":"Create",
"Current form": "Current form",
"Current object":"Current object"
}; };

View File

@ -614,5 +614,7 @@ export default {
"First or create":"存在しない場合に追加", "First or create":"存在しない場合に追加",
"Update or create":"存在しなければ新規、存在すれば更新", "Update or create":"存在しなければ新規、存在すれば更新",
"Find by the following fields":"次のフィールドで検索", "Find by the following fields":"次のフィールドで検索",
"Create":"新規のみ" "Create":"新規のみ" ,
"Current form":"現在のフォーム",
"Current object":"現在のオブジェクト"
} }

View File

@ -782,6 +782,8 @@ export default {
"Update or create":"不存在时新增,存在时更新", "Update or create":"不存在时新增,存在时更新",
"Find by the following fields":"通过以下字段查找", "Find by the following fields":"通过以下字段查找",
"Create":"仅新增", "Create":"仅新增",
"Current form":"当前表单",
"Current object":"当前对象",
"Quick create": "快速创建", "Quick create": "快速创建",
"Dropdown": "下拉菜单", "Dropdown": "下拉菜单",
"Pop-up": "弹窗", "Pop-up": "弹窗",

View File

@ -104,6 +104,7 @@ FormItem.Designer = function Designer() {
const { getCollectionFields, getInterface, getCollectionJoinField, getCollection } = useCollectionManager(); const { getCollectionFields, getInterface, getCollectionJoinField, getCollection } = useCollectionManager();
const { getField } = useCollection(); const { getField } = useCollection();
const { form } = useFormBlockContext(); const { form } = useFormBlockContext();
const ctx = useBlockRequestContext();
const field = useField<Field>(); const field = useField<Field>();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { t } = useTranslation(); const { t } = useTranslation();
@ -111,7 +112,6 @@ FormItem.Designer = function Designer() {
const compile = useCompile(); const compile = useCompile();
const variablesCtx = useVariablesCtx(); const variablesCtx = useVariablesCtx();
const IsShowMultipleSwitch = useIsShowMultipleSwitch(); const IsShowMultipleSwitch = useIsShowMultipleSwitch();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
if (collectionField?.target) { if (collectionField?.target) {
targetField = getCollectionJoinField( targetField = getCollectionJoinField(
@ -447,11 +447,16 @@ FormItem.Designer = function Designer() {
properties: { properties: {
filter: { filter: {
default: defaultFilter, default: defaultFilter,
// title: '数据范围',
enum: dataSource, enum: dataSource,
'x-component': 'Filter', 'x-component': 'Filter',
'x-component-props': { 'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }), dynamicComponent: (props) =>
FilterDynamicComponent({
...props,
form,
collectionField,
rootCollection: ctx.props.collection || ctx.props.resource,
}),
}, },
}, },
}, },
@ -461,7 +466,6 @@ FormItem.Designer = function Designer() {
filter = removeNullCondition(filter); filter = removeNullCondition(filter);
_.set(field.componentProps, 'service.params.filter', filter); _.set(field.componentProps, 'service.params.filter', filter);
fieldSchema['x-component-props'] = field.componentProps; fieldSchema['x-component-props'] = field.componentProps;
field.componentProps = field.componentProps;
dn.emit('patch', { dn.emit('patch', {
schema: { schema: {
['x-uid']: fieldSchema['x-uid'], ['x-uid']: fieldSchema['x-uid'],
@ -878,6 +882,11 @@ export function isFileCollection(collection: Collection) {
return collection?.template === 'file'; return collection?.template === 'file';
} }
function extractFirstPart(path) {
const firstDotIndex = path.indexOf('.');
return firstDotIndex !== -1 ? path.slice(0, firstDotIndex) : path;
}
FormItem.FilterFormDesigner = FilterFormDesigner; FormItem.FilterFormDesigner = FilterFormDesigner;
export function getFieldDefaultValue(fieldSchema: ISchema, collectionField: CollectionFieldOptions) { export function getFieldDefaultValue(fieldSchema: ISchema, collectionField: CollectionFieldOptions) {

View File

@ -1,7 +1,8 @@
import { LoadingOutlined } from '@ant-design/icons'; import { LoadingOutlined } from '@ant-design/icons';
import { connect, mapProps, mapReadPretty, useField, useFieldSchema } from '@formily/react'; import { connect, mapProps, mapReadPretty, useField, useFieldSchema, useForm } from '@formily/react';
import { SelectProps, Tag, Empty, Divider } from 'antd'; import { SelectProps, Tag, Empty, Divider } from 'antd';
import { uniqBy } from 'lodash'; import { uniqBy } from 'lodash';
import flat from 'flat';
import moment from 'moment'; import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ResourceActionOptions, useRequest } from '../../../api-client'; import { ResourceActionOptions, useRequest } from '../../../api-client';
@ -9,7 +10,9 @@ import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollection, useCollectionManager } from '../../../collection-manager';
import { Select, defaultFieldNames } from '../select'; import { Select, defaultFieldNames } from '../select';
import { ReadPretty } from './ReadPretty'; import { ReadPretty } from './ReadPretty';
import { useBlockRequestContext } from '../../../block-provider/BlockProvider';
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
import { parseVariables, extractFilterfield, generatePattern, extractValuesByPattern } from './utils';
const EMPTY = 'N/A'; const EMPTY = 'N/A';
export type RemoteSelectProps<P = any> = SelectProps<P, any> & { export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
@ -39,10 +42,12 @@ const InternalRemoteSelect = connect(
...others ...others
} = props; } = props;
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const form = useForm();
const firstRun = useRef(false); const firstRun = useRef(false);
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const isQuickAdd = fieldSchema['x-component-props']?.addMode === 'quickAdd'; const isQuickAdd = fieldSchema['x-component-props']?.addMode === 'quickAdd';
const field = useField(); const field = useField();
const ctx = useBlockRequestContext();
const { getField } = useCollection(); const { getField } = useCollection();
const searchData = useRef(null); const searchData = useRef(null);
const { getCollectionJoinField, getInterface } = useCollectionManager(); const { getCollectionJoinField, getInterface } = useCollectionManager();
@ -115,6 +120,48 @@ const InternalRemoteSelect = connect(
}, },
[targetField?.uiSchema, fieldNames], [targetField?.uiSchema, fieldNames],
); );
const parseFilter = (rules) => {
if (!rules) {
return undefined;
}
const type = Object.keys(rules)[0] || '$and';
const conditions = rules[type];
const results = [];
conditions?.forEach((c) => {
const jsonlogic = getInnermostKeyAndValue(c);
const regex = /{{(.*?)}}/;
const matches = jsonlogic.value?.match?.(regex);
if (!matches || (!matches[1].includes('$form') && !matches[1].includes('$iteration'))) {
results.push(c);
return;
}
const associationfield = extractFilterfield(matches[1]);
const filterCollectionField = getCollectionJoinField(`${ctx.props.collection}.${associationfield}`);
if (['o2m', 'm2m'].includes(filterCollectionField?.interface)) {
// 对多子表单
const pattern = generatePattern(matches?.[1], associationfield);
const parseValue: any = extractValuesByPattern(flat(form.values), pattern);
const filters = parseValue.map((v) => {
return JSON.parse(JSON.stringify(c).replace(jsonlogic.value, v));
});
results.push({ $or: filters });
} else {
const variablesCtx = { $form: form.values, $iteration: form.values };
let str = matches?.[1];
if (str.includes('$iteration')) {
const path = field.path.segments.concat([]);
path.pop();
str = str.replace('$iteration.', `$iteration.${path.join('.')}.`);
}
const parseValue = parseVariables(str, variablesCtx);
const filterObj = JSON.parse(
JSON.stringify(c).replace(jsonlogic.value, str.endsWith('id') ? parseValue ?? 0 : parseValue),
);
results.push(filterObj);
}
});
return { [type]: results };
};
const { data, run, loading } = useRequest( const { data, run, loading } = useRequest(
{ {
@ -123,9 +170,8 @@ const InternalRemoteSelect = connect(
params: { params: {
pageSize: 200, pageSize: 200,
...service?.params, ...service?.params,
// fields: [fieldNames.label, fieldNames.value, ...(service?.params?.fields || [])],
// search needs // search needs
filter: mergeFilter([field.componentProps?.service?.params?.filter || service?.params?.filter]), filter: mergeFilter([parseFilter(field.componentProps?.service?.params?.filter) || service?.params?.filter]),
}, },
}, },
{ {
@ -183,15 +229,11 @@ const InternalRemoteSelect = connect(
if (!data?.data?.length) { if (!data?.data?.length) {
return value != null ? (Array.isArray(value) ? value : [value]) : []; return value != null ? (Array.isArray(value) ? value : [value]) : [];
} }
const valueOptions = (value != null && (Array.isArray(value) ? value : [value])) || []; return uniqBy(data?.data || [], fieldNames.value);
return uniqBy(data?.data?.concat(valueOptions) || [], fieldNames.value);
}, [data?.data, value]); }, [data?.data, value]);
const onDropdownVisibleChange = (visible) => { const onDropdownVisibleChange = (visible) => {
setOpen(visible); setOpen(visible);
searchData.current = null; searchData.current = null;
if (firstRun.current && data?.data.length > 0) {
return;
}
run(); run();
firstRun.current = true; firstRun.current = true;
}; };
@ -238,7 +280,12 @@ const InternalRemoteSelect = connect(
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
return { return {
...props, ...props,
fieldNames: { ...defaultFieldNames, ...props.fieldNames, ...field.componentProps.fieldNames,...fieldSchema['x-component-props']?.fieldNames }, fieldNames: {
...defaultFieldNames,
...props.fieldNames,
...field.componentProps.fieldNames,
...fieldSchema['x-component-props']?.fieldNames,
},
suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffixIcon, suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffixIcon,
}; };
}, },

View File

@ -0,0 +1,35 @@
import { get, isFunction } from 'lodash';
export const parseVariables = (str: string, ctx) => {
if (str) {
const result = get(ctx, str);
return isFunction(result) ? result() : result;
} else {
return str;
}
};
export function extractFilterfield(str) {
const match = str.match(/^\$form\.([^.[\]]+)/);
if (match) {
return match[1];
}
return null;
}
export function extractValuesByPattern(obj, pattern) {
const regexPattern = new RegExp(pattern.replace(/\*/g, '\\d+'));
const result = [];
for (const key in obj) {
if (regexPattern.test(key)) {
const value = obj[key];
result.push(value);
}
}
return result;
}
export function generatePattern(str, fieldName) {
const result = str.replace(`$form.${fieldName}.`, `${fieldName}.*.`);
return result;
}

View File

@ -3,8 +3,8 @@ import { useVariableOptions } from '../../../schema-settings/VariableInput/hooks
import { Variable } from '../variable'; import { Variable } from '../variable';
export function FilterDynamicComponent(props) { export function FilterDynamicComponent(props) {
const { value, onChange, renderSchemaComponent } = props; const { value, onChange, renderSchemaComponent, form, collectionField, ...other } = props;
const options = useVariableOptions(); const options = useVariableOptions({ form, collectionField, ...other });
return ( return (
<Variable.Input value={value} onChange={onChange} scope={options}> <Variable.Input value={value} onChange={onChange} scope={options}>

View File

@ -1,3 +1,4 @@
import flat from 'flat'; import flat from 'flat';
import _, { every, findIndex, isArray, some } from 'lodash'; import _, { every, findIndex, isArray, some } from 'lodash';
import moment from 'moment'; import moment from 'moment';
@ -13,7 +14,6 @@ type VariablesCtx = {
export const useVariablesCtx = (): VariablesCtx => { export const useVariablesCtx = (): VariablesCtx => {
const { data } = useCurrentUserContext() || {}; const { data } = useCurrentUserContext() || {};
return useMemo(() => { return useMemo(() => {
return { return {
$user: data?.data || {}, $user: data?.data || {},
@ -44,7 +44,7 @@ export const parseVariables = (str: string, ctx: VariablesCtx) => {
} }
}; };
function getInnermostKeyAndValue(obj) { export function getInnermostKeyAndValue(obj) {
if (typeof obj !== 'object' || obj === null) { if (typeof obj !== 'object' || obj === null) {
return null; return null;
} }

View File

@ -0,0 +1,118 @@
import { useMemo } from 'react';
import { useCompile, useGetFilterOptions } from '../../../schema-component';
import { Schema } from '@formily/react';
import { FieldOption, Option } from '../type';
interface GetOptionsParams {
depth: number;
operator?: string;
maxDepth: number;
count?: number;
loadChildren?: (option: Option) => Promise<void>;
getFilterOptions?: (collectionName: string) => any[];
compile: (value: string) => any;
}
const getChildren = (
options: FieldOption[],
{ depth, maxDepth, loadChildren, compile }: GetOptionsParams,
): Option[] => {
const result = options
.map((option): Option => {
if (!option.target) {
return {
key: option.name,
value: option.name,
label: compile(option.title),
depth,
};
}
if (depth >= maxDepth) {
return null;
}
return {
key: option.name,
value: option.name,
label: compile(option.title),
children: [],
isLeaf: false,
field: option,
depth,
loadChildren,
};
})
.filter(Boolean);
return result;
};
export const useFormVariable = ({
blockForm,
rootCollection,
operator,
schema,
level,
}: {
blockForm?: any;
rootCollection: string;
operator?: any;
schema: Schema;
level?: number;
}) => {
const compile = useCompile();
const getFilterOptions = useGetFilterOptions();
const loadChildren = (option: any): Promise<void> => {
if (!option.field?.target) {
return new Promise((resolve) => {
resolve(void 0);
});
}
const collectionName = option.field.target;
const fields = getFilterOptions(collectionName);
const allowFields =
option.depth === 0
? fields.filter((field) => {
return Object.keys(blockForm.fields).some((name) => name.includes(`.${field.name}`));
})
: fields;
return new Promise((resolve) => {
setTimeout(() => {
const children =
getChildren(allowFields, {
depth: option.depth + 1,
maxDepth: 4,
loadChildren,
compile,
}) || [];
if (children.length === 0) {
option.disabled = true;
resolve();
return;
}
option.children = children;
resolve();
// 延迟 5 毫秒,防止阻塞主线程,导致 UI 卡顿
}, 5);
});
};
const result = useMemo(() => {
return (
blockForm && {
label: `{{t("Current form")}}`,
value: '$form',
key: '$form',
children: [],
isLeaf: false,
field: {
target: rootCollection,
},
depth: 0,
loadChildren,
}
);
}, [rootCollection]);
return result;
};

View File

@ -0,0 +1,94 @@
import { useMemo } from 'react';
import { useCollectionManager } from '../../../collection-manager';
import { useCompile, useGetFilterOptions } from '../../../schema-component';
interface GetOptionsParams {
schema: any;
operator?: string;
maxDepth: number;
count?: number;
getFilterOptions: (collectionName: string) => any[];
}
const getChildren = (options: any[], { schema, operator, maxDepth, count = 1, getFilterOptions }: GetOptionsParams) => {
if (count > maxDepth) {
return [];
}
const result = options.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'],
disabled: false,
};
}
const children =
getChildren(getFilterOptions(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;
};
export const useIterationVariable = ({
blockForm,
collectionField,
operator,
schema,
level,
rootCollection,
}: {
blockForm?: any;
collectionField: any;
operator?: any;
schema: any;
level?: number;
rootCollection?: string;
}) => {
const compile = useCompile();
const getFilterOptions = useGetFilterOptions();
const fields = getFilterOptions(collectionField?.collectionName);
const children = useMemo(() => {
const allowFields = fields.filter((field) => {
return Object.keys(blockForm.fields).some((name) => name.includes(field.name));
});
return (
getChildren(allowFields, {
schema,
operator,
maxDepth: level || 3,
getFilterOptions,
}) || []
);
}, [operator, schema, blockForm]);
return useMemo(() => {
return rootCollection !== collectionField?.collectionName && children.length > 0
? compile({
label: `{{t("Current object")}}`,
value: '$iteration',
key: '$iteration',
disabled: children.every((option) => option.disabled),
children: children,
})
: null;
}, [children]);
};

View File

@ -2,13 +2,20 @@ import { useMemo } from 'react';
import { useValues } from '../../../schema-component/antd/filter/useValues'; import { useValues } from '../../../schema-component/antd/filter/useValues';
import { useDateVariable } from './useDateVariable'; import { useDateVariable } from './useDateVariable';
import { useUserVariable } from './useUserVariable'; import { useUserVariable } from './useUserVariable';
import { useFormVariable } from './useFormVariable';
import { useIterationVariable } from './useIterationVariable';
export const useVariableOptions = () => { export const useVariableOptions = ({ form, collectionField, rootCollection }) => {
const { operator, schema } = useValues(); const { operator, schema } = useValues();
const userVariable = useUserVariable({ maxDepth: 3, schema }); const userVariable = useUserVariable({ maxDepth: 3, schema });
const dateVariable = useDateVariable({ operator, schema }); const dateVariable = useDateVariable({ operator, schema });
const formVariabele = useFormVariable({ blockForm: form, rootCollection, schema });
const iterationVariabele = useIterationVariable({ blockForm: form, collectionField, schema, rootCollection });
const result = useMemo(() => [userVariable, dateVariable], [dateVariable, userVariable]); const result = useMemo(
() => [userVariable, dateVariable, formVariabele, iterationVariabele].filter(Boolean),
[dateVariable, userVariable, formVariabele, iterationVariabele],
);
if (!operator || !schema) return []; if (!operator || !schema) return [];