refactor(association-field): useAssociationNames hook (#1956)

* chore: useAssociationNames

* refactor: code improve

* refactor: support blocktemplate

* refactor: support blocktemplate

* refactor: useAssociationNames in block template

* fix: cross hierarchical association field layout

* fix: normal field of association field should be append

* refactor: useAssociationNames

* refactor: useCreateActionProps

* refactor: useAssociationNames params

* fix: cleanup

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
katherinehhh 2023-06-07 11:18:35 +08:00 committed by GitHub
parent 83dc81c51b
commit 8809df3f51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 129 additions and 213 deletions

View File

@ -20,6 +20,9 @@ import { CollectionProvider, useCollection, useCollectionManager } from '../coll
import { FilterBlockRecord } from '../filter-provider/FilterProvider';
import { useRecordIndex } from '../record-provider';
import { SharedFilterProvider } from './SharedFilterProvider';
import _ from 'lodash';
import { useAssociationNames } from './hooks';
export const BlockResourceContext = createContext(null);
export const BlockAssociationContext = createContext(null);
@ -137,7 +140,6 @@ export const useResourceAction = (props, opts = {}) => {
refreshDeps: [runWhenParamsChanged ? null : JSON.stringify(params.appends)],
},
);
// automatic run service when params has changed
const firstRun = useRef(false);
useEffect(() => {
@ -164,7 +166,7 @@ export const MaybeCollectionProvider = (props) => {
);
};
const BlockRequestProvider = (props) => {
export const BlockRequestProvider = (props) => {
const field = useField();
const resource = useBlockResource();
const [allowedActions, setAllowedActions] = useState({});
@ -188,7 +190,16 @@ const BlockRequestProvider = (props) => {
const __parent = useContext(BlockRequestContext);
return (
<BlockRequestContext.Provider
value={{ allowedActions, block: props.block, props, field, service, resource, __parent }}
value={{
allowedActions,
block: props.block,
props,
field,
service,
resource,
__parent,
updateAssociationValues: props?.updateAssociationValues || [],
}}
>
{props.children}
</BlockRequestContext.Provider>
@ -264,13 +275,21 @@ export const RenderChildrenWithAssociationFilter: React.FC<any> = (props) => {
export const BlockProvider = (props) => {
const { collection, association } = props;
const resource = useResource(props);
const params = { ...props.params };
const { appends, updateAssociationValues } = useAssociationNames();
if (!Object.keys(params).includes('appends')) {
params['appends'] = appends;
}
return (
<MaybeCollectionProvider collection={collection}>
<BlockAssociationContext.Provider value={association}>
<BlockResourceContext.Provider value={resource}>
<BlockRequestProvider {...props}>
<SharedFilterProvider {...props}>
<FilterBlockRecord {...props}>{props.children}</FilterBlockRecord>
<BlockRequestProvider {...props} updateAssociationValues={updateAssociationValues} params={params}>
<SharedFilterProvider {...props} params={params}>
<FilterBlockRecord {...props} params={params}>
{props.children}
</FilterBlockRecord>
</SharedFilterProvider>
</BlockRequestProvider>
</BlockResourceContext.Provider>

View File

@ -4,7 +4,6 @@ import { Spin } from 'antd';
import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { RecordProvider } from '../record-provider';
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { useAssociationNames } from './hooks';
export const DetailsBlockContext = createContext<any>({});
@ -39,15 +38,9 @@ const InternalDetailsBlockProvider = (props) => {
};
export const DetailsBlockProvider = (props) => {
const params = { ...props.params };
const { collection } = props;
const { appends } = useAssociationNames(collection);
if (!Object.keys(params).includes('appends')) {
params['appends'] = appends;
}
return (
<BlockProvider {...props} params={params}>
<InternalDetailsBlockProvider {...props} params={params} />
<BlockProvider {...props}>
<InternalDetailsBlockProvider {...props} />
</BlockProvider>
);
};

View File

@ -7,12 +7,11 @@ import { RecordProvider, useRecord } from '../record-provider';
import { useActionContext, useDesignable } from '../schema-component';
import { Templates as DataTemplateSelect } from '../schema-component/antd/form-v2/Templates';
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { useAssociationNames } from './hooks';
export const FormBlockContext = createContext<any>({});
const InternalFormBlockProvider = (props) => {
const { action, readPretty, params, updateAssociationValues } = props;
const { action, readPretty, params } = props;
const field = useField();
const form = useMemo(
() =>
@ -21,7 +20,7 @@ const InternalFormBlockProvider = (props) => {
}),
[],
);
const { resource, service } = useBlockRequestContext();
const { resource, service, updateAssociationValues } = useBlockRequestContext();
const formBlockRef = useRef();
const record = useRecord();
// if (service.loading) {
@ -68,14 +67,9 @@ export const FormBlockProvider = (props) => {
const record = useRecord();
const { collection } = props;
const { __collection } = record;
const params = { ...props.params };
const currentCollection = useCollection();
const { designable } = useDesignable();
const isEmptyRecord = useIsEmptyRecord();
const { appends, updateAssociationValues } = useAssociationNames(collection);
if (!Object.keys(params).includes('appends')) {
params['appends'] = appends;
}
let detailFlag = false;
if (isEmptyRecord) {
detailFlag = true;
@ -87,8 +81,8 @@ export const FormBlockProvider = (props) => {
(currentCollection.name === (collection?.name || collection) && !isEmptyRecord) || !currentCollection.name;
return (
(detailFlag || createFlag) && (
<BlockProvider {...props} block={'form'} params={params}>
<InternalFormBlockProvider {...props} params={params} updateAssociationValues={updateAssociationValues} />
<BlockProvider {...props} block={'form'}>
<InternalFormBlockProvider {...props} />
</BlockProvider>
)
);

View File

@ -1,14 +1,13 @@
import { ArrayField, createForm } from '@formily/core';
import { FormContext, Schema, useField, useFieldSchema } from '@formily/react';
import uniq from 'lodash/uniq';
import { FormContext, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useCollection, useCollectionManager } from '../collection-manager';
import { useFilterBlock } from '../filter-provider/FilterProvider';
import { useRecord } from '../record-provider';
import { FixedBlockWrapper, SchemaComponentOptions, removeNullCondition } from '../schema-component';
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
import { mergeFilter } from './SharedFilterProvider';
import { findFilterTargets } from './hooks';
import { useRecord } from '../record-provider';
export const TableBlockContext = createContext<any>({});
@ -47,62 +46,60 @@ const InternalTableBlockProvider = (props: Props) => {
);
};
const useAssociationNames = (collection) => {
const { getCollectionFields } = useCollectionManager();
const collectionFields = getCollectionFields(collection);
const associationFields = new Set();
for (const collectionField of collectionFields) {
if (collectionField.target) {
associationFields.add(collectionField.name);
const fields = getCollectionFields(collectionField.target);
for (const field of fields) {
if (field.target) {
associationFields.add(`${collectionField.name}.${field.name}`);
}
}
}
}
const fieldSchema = useFieldSchema();
const tableSchema = fieldSchema.reduceProperties((buf, schema) => {
if (schema['x-component'] === 'TableV2') {
return schema;
}
if (schema['x-component'] === 'Gantt') {
return schema.properties?.table;
}
return buf;
}, new Schema({}));
return uniq(
tableSchema.reduceProperties((buf, schema) => {
if (schema['x-component'] === 'TableV2.Column') {
const s = schema.reduceProperties((buf, s) => {
const [name] = (s.name as string).split('.');
if (s['x-collection-field'] && associationFields.has(name)) {
return s;
}
return buf;
}, null);
if (s) {
// 关联字段和关联的关联字段
const [firstName] = s.name.split('.');
if (associationFields.has(s.name)) {
buf.push(s.name);
} else if (associationFields.has(firstName)) {
buf.push(firstName);
}
}
}
return buf;
}, []),
);
};
// export const useAssociationNames = (collection) => {
// const { getCollectionFields } = useCollectionManager();
// const collectionFields = getCollectionFields(collection);
// const associationFields = new Set();
// for (const collectionField of collectionFields) {
// if (collectionField.target) {
// associationFields.add(collectionField.name);
// const fields = getCollectionFields(collectionField.target);
// for (const field of fields) {
// if (field.target) {
// associationFields.add(`${collectionField.name}.${field.name}`);
// }
// }
// }
// }
// const fieldSchema = useFieldSchema();
// const tableSchema = fieldSchema.reduceProperties((buf, schema) => {
// if (schema['x-component'] === 'TableV2') {
// return schema;
// }
// if (schema['x-component'] === 'Gantt') {
// return schema.properties?.table;
// }
// return buf;
// }, new Schema({}));
// return uniq(
// tableSchema.reduceProperties((buf, schema) => {
// if (schema['x-component'] === 'TableV2.Column') {
// const s = schema.reduceProperties((buf, s) => {
// const [name] = (s.name as string).split('.');
// if (s['x-collection-field'] && associationFields.has(name)) {
// return s;
// }
// return buf;
// }, null);
// if (s) {
// // 关联字段和关联的关联字段
// const [firstName] = s.name.split('.');
// if (associationFields.has(s.name)) {
// buf.push(s.name);
// } else if (associationFields.has(firstName)) {
// buf.push(firstName);
// }
// }
// }
// return buf;
// }, []),
// );
// };
export const TableBlockProvider = (props) => {
const resourceName = props.resource;
const params = { ...props.params };
const record = useRecord();
const appends = useAssociationNames(props.collection);
const fieldSchema = useFieldSchema();
const { getCollection, getCollectionField } = useCollectionManager();
const parent = useCollection();
@ -131,9 +128,6 @@ export const TableBlockProvider = (props) => {
params['tree'] = true;
}
}
if (!Object.keys(params).includes('appends')) {
params['appends'] = appends;
}
const form = useMemo(() => createForm(), [treeTable]);
return (
<SchemaComponentOptions scope={{ treeTable }}>

View File

@ -1,7 +1,7 @@
import { Schema, SchemaExpressionScopeContext, useField, useFieldSchema, useForm } from '@formily/react';
import { SchemaExpressionScopeContext, useField, useFieldSchema, useForm } from '@formily/react';
import { parse } from '@nocobase/utils/client';
import { Modal, message } from 'antd';
import { cloneDeep, uniq } from 'lodash';
import { cloneDeep } from 'lodash';
import get from 'lodash/get';
import omit from 'lodash/omit';
import { ChangeEvent, useContext, useEffect } from 'react';
@ -16,7 +16,6 @@ import { transformToFilter } from '../../filter-provider/utils';
import { useRecord } from '../../record-provider';
import { removeNullCondition, useActionContext, useCompile } from '../../schema-component';
import { BulkEditFormItemValueType } from '../../schema-initializer/components';
import { useSchemaTemplateManager } from '../../schema-templates';
import { useCurrentUserContext } from '../../user';
import { useBlockRequestContext, useFilterByTk } from '../BlockProvider';
import { useDetailsBlockContext } from '../DetailsBlockProvider';
@ -1075,120 +1074,53 @@ export const useAssociationFilterBlockProps = () => {
};
};
const getTemplateSchema = (schema) => {
const conf = {
url: `/uiSchemas:getJsonSchema/${schema?.uid}`,
};
const { data, loading, run } = useRequest(conf, { manual: true });
if (loading) {
function getAssociationPath(str) {
const lastIndex = str.lastIndexOf('.');
if (lastIndex !== -1) {
return str.substring(0, lastIndex);
}
useEffect(() => {
if (schema?.uid) {
run();
}
}, [schema?.uid]);
return schema?.uid ? new Schema(data?.data) : null;
};
return str;
}
export const useAssociationNames = (collection) => {
export const useAssociationNames = () => {
const { getCollectionJoinField } = useCollectionManager();
const { getTemplateById } = useSchemaTemplateManager();
const fieldSchema = useFieldSchema();
const associationValues = [];
const formSchema = fieldSchema.reduceProperties((buf, schema) => {
if (['FormV2', 'Details', 'List', 'GridCard'].includes(schema['x-component'])) {
return schema;
}
return buf;
}, new Schema({}));
const templateSchema = formSchema.reduceProperties((buf, schema) => {
if (schema['x-component'] === 'BlockTemplate') {
return schema;
}
return buf;
}, null);
const getAssociationAppends = (schema, arr = []) => {
const data = schema.reduceProperties((buf, s) => {
const updateAssociationValues = new Set([]);
const appends = new Set([]);
const getAssociationAppends = (schema, str) => {
schema.reduceProperties((pre, s) => {
const prefix = pre || str;
const collectionfield = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field']);
if (
collectionfield &&
['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(collectionfield.type) &&
s['x-component'] !== 'TableField'
) {
buf.push(s.name);
const isAssociationSubfield = s.name.includes('.');
const isAssociationField =
collectionfield && ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(collectionfield.type);
if (collectionfield && (isAssociationField || isAssociationSubfield) && s['x-component'] !== 'TableField') {
const fieldPath = !isAssociationField && isAssociationSubfield ? getAssociationPath(s.name) : s.name;
const path = prefix === '' || !prefix ? fieldPath : prefix + '.' + fieldPath;
appends.add(path);
if (['Nester', 'SubTable'].includes(s['x-component-props']?.mode)) {
associationValues.push(s.name);
}
if (s['x-component-props']?.mode === 'Nester') {
return getAssociationAppends(s, buf);
}
return buf;
} else {
if (s['x-component'] === 'Grid.Row') {
const kk = buf?.concat?.();
return getNesterAppends(s, kk || []);
} else {
return !s['x-component']?.includes('Action.') && s['x-component'] !== 'TableField'
? getAssociationAppends(s, buf)
: buf;
updateAssociationValues.add(path);
const bufPrefix = prefix && prefix !== '' ? prefix + '.' + s.name : s.name;
getAssociationAppends(s, bufPrefix);
}
} else if (
![
'ActionBar',
'Action',
'Action.Link',
'Action.Modal',
'Selector',
'Viewer',
'AddNewer',
'AssociationField.Selector',
'AssociationField.AddNewer',
'TableField',
].includes(s['x-component'])
) {
getAssociationAppends(s, str);
}
}, arr);
return data || [];
}, str);
};
function flattenNestedList(nestedList) {
const flattenedList = [];
function flattenHelper(list, prefix) {
for (let i = 0; i < list.length; i++) {
if (Array.isArray(list[i])) {
`${prefix}` !== `${list[i][0]}` && flattenHelper(list[i], `${prefix}.${list[i][0]}`);
} else {
const searchTerm = `.${list[i]}`;
const lastIndex = prefix.lastIndexOf(searchTerm);
let str = '';
if (lastIndex !== -1) {
str = prefix.slice(0, lastIndex) + prefix.slice(lastIndex + searchTerm.length);
}
if (!str) {
!list.includes(str) && flattenedList.push(`${list[i]}`);
} else {
!list.includes(str) ? flattenedList.push(`${str}.${list[i]}`) : flattenedList.push(str);
}
}
}
}
for (let i = 0; i < nestedList.length; i++) {
flattenHelper(nestedList[i], nestedList[i][0]);
}
return uniq(flattenedList.filter((obj) => !obj?.startsWith('.')));
}
const getNesterAppends = (gridSchema, data) => {
gridSchema.reduceProperties((buf, s) => {
buf.push(getAssociationAppends(s));
return buf;
}, data);
return data.filter((g) => g.length);
};
const template = getTemplateById(templateSchema?.['x-component-props']?.templateId);
const schema = getTemplateSchema(template);
if (schema) {
const associations = getAssociationAppends(schema);
const appends = flattenNestedList(associations);
return {
appends,
updateAssociationValues: appends.filter((item) => associationValues.some((suffix) => item.endsWith(suffix))),
};
}
if (!schema) {
const associations = getAssociationAppends(formSchema);
const appends = flattenNestedList(associations);
return {
appends,
updateAssociationValues: appends.filter((item) => associationValues.some((suffix) => item.endsWith(suffix))),
};
}
getAssociationAppends(fieldSchema, '');
return { appends: [...appends], updateAssociationValues: [...updateAssociationValues] };
};

View File

@ -2,9 +2,9 @@ import { LoadingOutlined } from '@ant-design/icons';
import { RecursionField, connect, mapProps, observer, useField, useFieldSchema } from '@formily/react';
import { Input } from 'antd';
import React from 'react';
import { RecordProvider } from '../../../';
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
import useServiceOptions from './hooks';
import { RecordProvider } from '../../../';
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
action?: string;

View File

@ -4,8 +4,6 @@ import React, { createContext, useContext, useEffect, useMemo, useState } from '
import { BlockProvider, useBlockRequestContext } from '../../../block-provider';
import { FormLayout } from '@formily/antd';
import { css } from '@emotion/css';
import { useAssociationNames } from '../../../block-provider/hooks';
export const GridCardBlockContext = createContext<any>({});
const InternalGridCardBlockProvider = (props) => {
@ -57,15 +55,8 @@ const InternalGridCardBlockProvider = (props) => {
};
export const GridCardBlockProvider = (props) => {
const params = { ...props.params };
const { collection } = props;
const { appends } = useAssociationNames(collection);
if (!Object.keys(params).includes('appends')) {
params['appends'] = appends;
}
return (
<BlockProvider {...props} params={params}>
<BlockProvider {...props}>
<InternalGridCardBlockProvider {...props} />
</BlockProvider>
);

View File

@ -2,7 +2,6 @@ import { createForm } from '@formily/core';
import { FormContext, useField } from '@formily/react';
import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { FormLayout } from '@formily/antd';
import { useAssociationNames } from '../../../block-provider/hooks';
import { BlockProvider, useBlockRequestContext } from '../../../block-provider';
export const ListBlockContext = createContext<any>({});
@ -38,15 +37,8 @@ const InternalListBlockProvider = (props) => {
};
export const ListBlockProvider = (props) => {
const params = { ...props.params };
const { collection } = props;
const { appends } = useAssociationNames(collection);
if (!Object.keys(params).includes('appends')) {
params['appends'] = appends;
}
return (
<BlockProvider {...props} params={params}>
<BlockProvider {...props}>
<InternalListBlockProvider {...props} />
</BlockProvider>
);

View File

@ -21,6 +21,7 @@ export const BlockTemplate = observer(
const template = useMemo(() => getTemplateById(templateId), [templateId]);
const onSuccess = (data) => {
fieldSchema['x-linkage-rules'] = data?.data?.['x-linkage-rules'] || [];
fieldSchema.setProperties(data?.data?.properties);
};
return template ? (
<BlockTemplateContext.Provider value={{ dn, field, fieldSchema, template }}>

View File

@ -18,7 +18,7 @@ export function FormBlockProvider(props) {
const fieldSchema = useFieldSchema();
const field = useField();
const formBlockRef = useRef(null);
const { appends, updateAssociationValues } = useAssociationNames(props.collection);
const { appends, updateAssociationValues } = useAssociationNames();
const [formKey] = Object.keys(fieldSchema.toJSON().properties ?? {});
const values = userJob?.result?.[formKey];