From cb52b80cf0d6eff948969551c2eb53744596966c Mon Sep 17 00:00:00 2001 From: katherinehhh Date: Thu, 22 Jun 2023 20:19:34 +0800 Subject: [PATCH] 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 --- packages/core/client/src/locale/en_US.ts | 4 +- packages/core/client/src/locale/ja_JP.ts | 4 +- packages/core/client/src/locale/zh_CN.ts | 2 + .../antd/form-item/FormItem.tsx | 17 ++- .../antd/remote-select/RemoteSelect.tsx | 67 ++++++++-- .../antd/remote-select/utils.ts | 35 ++++++ .../antd/table-v2/FilterDynamicComponent.tsx | 4 +- .../schema-component/common/utils/uitls.tsx | 4 +- .../VariableInput/hooks/useFormVariable.ts | 118 ++++++++++++++++++ .../hooks/useIterationVariable.ts | 94 ++++++++++++++ .../VariableInput/hooks/useVariableOptions.ts | 11 +- 11 files changed, 338 insertions(+), 22 deletions(-) create mode 100644 packages/core/client/src/schema-component/antd/remote-select/utils.ts create mode 100644 packages/core/client/src/schema-settings/VariableInput/hooks/useFormVariable.ts create mode 100644 packages/core/client/src/schema-settings/VariableInput/hooks/useIterationVariable.ts diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index 05e834f12..742fa725d 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -703,5 +703,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" + "Create":"Create", + "Current form": "Current form", + "Current object":"Current object" }; diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts index c446cfabc..b7f5717f0 100644 --- a/packages/core/client/src/locale/ja_JP.ts +++ b/packages/core/client/src/locale/ja_JP.ts @@ -614,5 +614,7 @@ export default { "First or create":"存在しない場合に追加", "Update or create":"存在しなければ新規、存在すれば更新", "Find by the following fields":"次のフィールドで検索", - "Create":"新規のみ" + "Create":"新規のみ" , + "Current form":"現在のフォーム", + "Current object":"現在のオブジェクト" } diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 5fa1dcc15..2d9e62a01 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -782,6 +782,8 @@ export default { "Update or create":"不存在时新增,存在时更新", "Find by the following fields":"通过以下字段查找", "Create":"仅新增", + "Current form":"当前表单", + "Current object":"当前对象", "Quick create": "快速创建", "Dropdown": "下拉菜单", "Pop-up": "弹窗", diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx index 89ecd7280..a9418e6f2 100644 --- a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx @@ -104,6 +104,7 @@ FormItem.Designer = function Designer() { const { getCollectionFields, getInterface, getCollectionJoinField, getCollection } = useCollectionManager(); const { getField } = useCollection(); const { form } = useFormBlockContext(); + const ctx = useBlockRequestContext(); const field = useField(); const fieldSchema = useFieldSchema(); const { t } = useTranslation(); @@ -111,7 +112,6 @@ FormItem.Designer = function Designer() { const compile = useCompile(); const variablesCtx = useVariablesCtx(); const IsShowMultipleSwitch = useIsShowMultipleSwitch(); - const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); if (collectionField?.target) { targetField = getCollectionJoinField( @@ -447,11 +447,16 @@ FormItem.Designer = function Designer() { properties: { filter: { default: defaultFilter, - // title: '数据范围', enum: dataSource, 'x-component': 'Filter', '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); _.set(field.componentProps, 'service.params.filter', filter); fieldSchema['x-component-props'] = field.componentProps; - field.componentProps = field.componentProps; dn.emit('patch', { schema: { ['x-uid']: fieldSchema['x-uid'], @@ -878,6 +882,11 @@ export function isFileCollection(collection: Collection) { return collection?.template === 'file'; } +function extractFirstPart(path) { + const firstDotIndex = path.indexOf('.'); + return firstDotIndex !== -1 ? path.slice(0, firstDotIndex) : path; +} + FormItem.FilterFormDesigner = FilterFormDesigner; export function getFieldDefaultValue(fieldSchema: ISchema, collectionField: CollectionFieldOptions) { diff --git a/packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx b/packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx index f1a2ff3d7..29fcd8efb 100644 --- a/packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx +++ b/packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx @@ -1,7 +1,8 @@ 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 { uniqBy } from 'lodash'; +import flat from 'flat'; import moment from 'moment'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ResourceActionOptions, useRequest } from '../../../api-client'; @@ -9,7 +10,9 @@ import { mergeFilter } from '../../../block-provider/SharedFilterProvider'; import { useCollection, useCollectionManager } from '../../../collection-manager'; import { Select, defaultFieldNames } from '../select'; 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'; export type RemoteSelectProps

= SelectProps & { @@ -39,10 +42,12 @@ const InternalRemoteSelect = connect( ...others } = props; const [open, setOpen] = useState(false); + const form = useForm(); const firstRun = useRef(false); const fieldSchema = useFieldSchema(); const isQuickAdd = fieldSchema['x-component-props']?.addMode === 'quickAdd'; const field = useField(); + const ctx = useBlockRequestContext(); const { getField } = useCollection(); const searchData = useRef(null); const { getCollectionJoinField, getInterface } = useCollectionManager(); @@ -115,6 +120,48 @@ const InternalRemoteSelect = connect( }, [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( { @@ -123,9 +170,8 @@ const InternalRemoteSelect = connect( params: { pageSize: 200, ...service?.params, - // fields: [fieldNames.label, fieldNames.value, ...(service?.params?.fields || [])], // 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) { return value != null ? (Array.isArray(value) ? value : [value]) : []; } - const valueOptions = (value != null && (Array.isArray(value) ? value : [value])) || []; - return uniqBy(data?.data?.concat(valueOptions) || [], fieldNames.value); + return uniqBy(data?.data || [], fieldNames.value); }, [data?.data, value]); const onDropdownVisibleChange = (visible) => { setOpen(visible); searchData.current = null; - if (firstRun.current && data?.data.length > 0) { - return; - } run(); firstRun.current = true; }; @@ -238,7 +280,12 @@ const InternalRemoteSelect = connect( const fieldSchema = useFieldSchema(); return { ...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'] ? : props.suffixIcon, }; }, diff --git a/packages/core/client/src/schema-component/antd/remote-select/utils.ts b/packages/core/client/src/schema-component/antd/remote-select/utils.ts new file mode 100644 index 000000000..bb6ba7759 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/remote-select/utils.ts @@ -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; +} diff --git a/packages/core/client/src/schema-component/antd/table-v2/FilterDynamicComponent.tsx b/packages/core/client/src/schema-component/antd/table-v2/FilterDynamicComponent.tsx index 258f65237..7ba7ce165 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/FilterDynamicComponent.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/FilterDynamicComponent.tsx @@ -3,8 +3,8 @@ import { useVariableOptions } from '../../../schema-settings/VariableInput/hooks import { Variable } from '../variable'; export function FilterDynamicComponent(props) { - const { value, onChange, renderSchemaComponent } = props; - const options = useVariableOptions(); + const { value, onChange, renderSchemaComponent, form, collectionField, ...other } = props; + const options = useVariableOptions({ form, collectionField, ...other }); return ( diff --git a/packages/core/client/src/schema-component/common/utils/uitls.tsx b/packages/core/client/src/schema-component/common/utils/uitls.tsx index eaeeebf2a..b6460c607 100644 --- a/packages/core/client/src/schema-component/common/utils/uitls.tsx +++ b/packages/core/client/src/schema-component/common/utils/uitls.tsx @@ -1,3 +1,4 @@ + import flat from 'flat'; import _, { every, findIndex, isArray, some } from 'lodash'; import moment from 'moment'; @@ -13,7 +14,6 @@ type VariablesCtx = { export const useVariablesCtx = (): VariablesCtx => { const { data } = useCurrentUserContext() || {}; - return useMemo(() => { return { $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) { return null; } diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useFormVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useFormVariable.ts new file mode 100644 index 000000000..7f95b6c8e --- /dev/null +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useFormVariable.ts @@ -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; + 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 => { + 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; +}; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useIterationVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useIterationVariable.ts new file mode 100644 index 000000000..ea0fae822 --- /dev/null +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useIterationVariable.ts @@ -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]); +}; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts index 024bb3b65..9a39fe002 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts @@ -2,13 +2,20 @@ import { useMemo } from 'react'; import { useValues } from '../../../schema-component/antd/filter/useValues'; import { useDateVariable } from './useDateVariable'; 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 userVariable = useUserVariable({ maxDepth: 3, 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 [];