feat: association support select cascade for tree collection field (#2514)
* feat: association field support cascade select * refactor: code improve * refactor: code improve * refactor: code improve * refactor: locale improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: cascadeSelect support m2m association field * refactor: cascadeSelect support m2m association field * refactor: code improve * feat(database): append with options * feat: recursively load parent instances * chore: test * refactor: code improve * fix: load with appends * refactor: code improve * chore: test * refactor: code improve * refactor: code improve * refactor: code improve * chore: load with belongs to many * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve --------- Co-authored-by: ChengLei Shao <chareice@live.com> Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
505c23b4e1
commit
7dd7a65a38
@ -1114,7 +1114,7 @@ export function getAssociationPath(str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useAssociationNames = () => {
|
export const useAssociationNames = () => {
|
||||||
const { getCollectionJoinField } = useCollectionManager();
|
const { getCollectionJoinField, getCollection } = useCollectionManager();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const updateAssociationValues = new Set([]);
|
const updateAssociationValues = new Set([]);
|
||||||
const appends = new Set([]);
|
const appends = new Set([]);
|
||||||
@ -1125,10 +1125,16 @@ export const useAssociationNames = () => {
|
|||||||
const isAssociationSubfield = s.name.includes('.');
|
const isAssociationSubfield = s.name.includes('.');
|
||||||
const isAssociationField =
|
const isAssociationField =
|
||||||
collectionfield && ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(collectionfield.type);
|
collectionfield && ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(collectionfield.type);
|
||||||
|
const isTreeCollection = isAssociationField && getCollection(collectionfield.target).template === 'tree';
|
||||||
if (collectionfield && (isAssociationField || isAssociationSubfield) && s['x-component'] !== 'TableField') {
|
if (collectionfield && (isAssociationField || isAssociationSubfield) && s['x-component'] !== 'TableField') {
|
||||||
const fieldPath = !isAssociationField && isAssociationSubfield ? getAssociationPath(s.name) : s.name;
|
const fieldPath = !isAssociationField && isAssociationSubfield ? getAssociationPath(s.name) : s.name;
|
||||||
const path = prefix === '' || !prefix ? fieldPath : prefix + '.' + fieldPath;
|
const path = prefix === '' || !prefix ? fieldPath : prefix + '.' + fieldPath;
|
||||||
|
if (isTreeCollection) {
|
||||||
appends.add(path);
|
appends.add(path);
|
||||||
|
appends.add(`${path}.parent` + '(recursively=true)');
|
||||||
|
} else {
|
||||||
|
appends.add(path);
|
||||||
|
}
|
||||||
if (['Nester', 'SubTable', 'PopoverNester'].includes(s['x-component-props']?.mode)) {
|
if (['Nester', 'SubTable', 'PopoverNester'].includes(s['x-component-props']?.mode)) {
|
||||||
updateAssociationValues.add(path);
|
updateAssociationValues.add(path);
|
||||||
const bufPrefix = prefix && prefix !== '' ? prefix + '.' + s.name : s.name;
|
const bufPrefix = prefix && prefix !== '' ? prefix + '.' + s.name : s.name;
|
||||||
|
@ -778,6 +778,7 @@ export default {
|
|||||||
"Select all":"Select all",
|
"Select all":"Select all",
|
||||||
"Restart": "Restart",
|
"Restart": "Restart",
|
||||||
"Restart application": "Restart application",
|
"Restart application": "Restart application",
|
||||||
|
"Cascade Select":"Cascade Select",
|
||||||
Execute: 'Execute',
|
Execute: 'Execute',
|
||||||
'Please use a valid SELECT or WITH AS statement': 'Please use a valid SELECT or WITH AS statement',
|
'Please use a valid SELECT or WITH AS statement': 'Please use a valid SELECT or WITH AS statement',
|
||||||
'Please confirm the SQL statement first': 'Please confirm the SQL statement first',
|
'Please confirm the SQL statement first': 'Please confirm the SQL statement first',
|
||||||
|
@ -627,6 +627,7 @@ export default {
|
|||||||
"Sync successfully":"同期成功",
|
"Sync successfully":"同期成功",
|
||||||
"Sync from form fields":"フォームフィールドの同期",
|
"Sync from form fields":"フォームフィールドの同期",
|
||||||
"Select all": "すべて選択",
|
"Select all": "すべて選択",
|
||||||
|
"Cascade Select":"カスケード選択",
|
||||||
"New plugin": "新しいプラグイン",
|
"New plugin": "新しいプラグイン",
|
||||||
"Upgrade": "アップグレード",
|
"Upgrade": "アップグレード",
|
||||||
"Dependencies check failed": "依存関係のチェックに失敗しました",
|
"Dependencies check failed": "依存関係のチェックに失敗しました",
|
||||||
|
@ -872,6 +872,7 @@ export default {
|
|||||||
'Sync from form fields': '同步表单字段',
|
'Sync from form fields': '同步表单字段',
|
||||||
'Select all': '全选',
|
'Select all': '全选',
|
||||||
'Determine whether a record exists by the following fields': '通过以下字段判断记录是否存在',
|
'Determine whether a record exists by the following fields': '通过以下字段判断记录是否存在',
|
||||||
|
'Cascade Select': '级联选择',
|
||||||
Execute: '执行',
|
Execute: '执行',
|
||||||
'Please use a valid SELECT or WITH AS statement': '请使用有效的 SELECT 或 WITH AS 语句',
|
'Please use a valid SELECT or WITH AS statement': '请使用有效的 SELECT 或 WITH AS 语句',
|
||||||
'Please confirm the SQL statement first': '请先确认 SQL 语句',
|
'Please confirm the SQL statement first': '请先确认 SQL 语句',
|
||||||
|
@ -109,27 +109,4 @@ interface AssociationSelectInterface {
|
|||||||
FilterDesigner: React.FC;
|
FilterDesigner: React.FC;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AssociationSelect = (InternalAssociationSelect as unknown) as AssociationSelectInterface;
|
export const AssociationSelect = InternalAssociationSelect as unknown as AssociationSelectInterface;
|
||||||
|
|
||||||
export const AssociationSelectReadPretty = connect(
|
|
||||||
(props: any) => {
|
|
||||||
const service = useServiceOptions(props);
|
|
||||||
if (props.fieldNames) {
|
|
||||||
return <RemoteSelect.ReadPretty {...props} service={service}></RemoteSelect.ReadPretty>;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
mapProps(
|
|
||||||
{
|
|
||||||
dataSource: 'options',
|
|
||||||
loading: true,
|
|
||||||
},
|
|
||||||
(props, field) => {
|
|
||||||
return {
|
|
||||||
...props,
|
|
||||||
fieldNames: props.fieldNames && { ...props.fieldNames, ...field.componentProps.fieldNames },
|
|
||||||
suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffixIcon,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
@ -10,6 +10,7 @@ import { InternalNester } from './InternalNester';
|
|||||||
import { InternalPicker } from './InternalPicker';
|
import { InternalPicker } from './InternalPicker';
|
||||||
import { InternalSubTable } from './InternalSubTable';
|
import { InternalSubTable } from './InternalSubTable';
|
||||||
import { InternaPopoverNester } from './InternalPopoverNester';
|
import { InternaPopoverNester } from './InternalPopoverNester';
|
||||||
|
import { InternalCascadeSelect } from './InternalCascadeSelect';
|
||||||
import { CreateRecordAction } from './components/CreateRecordAction';
|
import { CreateRecordAction } from './components/CreateRecordAction';
|
||||||
import { useAssociationFieldContext } from './hooks';
|
import { useAssociationFieldContext } from './hooks';
|
||||||
import { useCollection } from '../../../collection-manager';
|
import { useCollection } from '../../../collection-manager';
|
||||||
@ -55,6 +56,7 @@ const EditableAssociationField = observer(
|
|||||||
{currentMode === 'Select' && <AssociationSelect {...props} />}
|
{currentMode === 'Select' && <AssociationSelect {...props} />}
|
||||||
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
|
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
|
||||||
{currentMode === 'FileManager' && <InternalFileManager {...props} />}
|
{currentMode === 'FileManager' && <InternalFileManager {...props} />}
|
||||||
|
{currentMode === 'CascadeSelect' && <InternalCascadeSelect {...props} />}
|
||||||
</SchemaComponentOptions>
|
</SchemaComponentOptions>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,379 @@
|
|||||||
|
import { observer, useField, connect, createSchemaField, FormProvider, useFieldSchema, Field } from '@formily/react';
|
||||||
|
import { createForm, onFormValuesChange } from '@formily/core';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
import { Space, Tag, Spin, Select as AntdSelect, Input } from 'antd';
|
||||||
|
import { ArrayItems, FormItem } from '@formily/antd-v5';
|
||||||
|
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useCompile, SchemaComponent } from '../../../schema-component';
|
||||||
|
import { useAPIClient, useCollectionManager } from '../../../';
|
||||||
|
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
||||||
|
import useServiceOptions, { useAssociationFieldContext } from './hooks';
|
||||||
|
|
||||||
|
const EMPTY = 'N/A';
|
||||||
|
const SchemaField = createSchemaField({
|
||||||
|
components: {
|
||||||
|
Space,
|
||||||
|
Input,
|
||||||
|
ArrayItems,
|
||||||
|
FormItem,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const CascadeSelect = connect((props) => {
|
||||||
|
const { data, mapOptions, onChange } = props;
|
||||||
|
const [selectedOptions, setSelectedOptions] = useState<{ key: string; children: any; value?: any }[]>([
|
||||||
|
{ key: undefined, children: [], value: null },
|
||||||
|
]);
|
||||||
|
const [options, setOptions] = useState(data);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const compile = useCompile();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const service = useServiceOptions(props);
|
||||||
|
const { options: collectionField, field: associationField } = useAssociationFieldContext<any>();
|
||||||
|
const resource = api.resource(collectionField.target);
|
||||||
|
const { getCollectionJoinField, getInterface } = useCollectionManager();
|
||||||
|
const fieldNames = associationField?.componentProps?.fieldNames;
|
||||||
|
const targetField =
|
||||||
|
collectionField?.target &&
|
||||||
|
fieldNames?.label &&
|
||||||
|
getCollectionJoinField(`${collectionField.target}.${fieldNames.label}`);
|
||||||
|
const operator = useMemo(() => {
|
||||||
|
if (targetField?.interface) {
|
||||||
|
return getInterface(targetField.interface)?.filterable?.operators[0].value || '$includes';
|
||||||
|
}
|
||||||
|
return '$includes';
|
||||||
|
}, [targetField]);
|
||||||
|
const field: any = useField();
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.value) {
|
||||||
|
const values = Array.isArray(props.value)
|
||||||
|
? extractLastNonNullValueObjects(props.value?.filter((v) => v.value), true)
|
||||||
|
: transformNestedData(props.value);
|
||||||
|
const options = values?.map?.((v) => {
|
||||||
|
return {
|
||||||
|
key: v.parentId,
|
||||||
|
children: [],
|
||||||
|
value: v,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setSelectedOptions(options);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const mapOptionsToTags = useCallback(
|
||||||
|
(options) => {
|
||||||
|
try {
|
||||||
|
return options
|
||||||
|
?.filter((v) => ['number', 'string'].includes(typeof v[fieldNames.value]))
|
||||||
|
.map((option) => {
|
||||||
|
let label = compile(option[fieldNames.label]);
|
||||||
|
|
||||||
|
if (targetField?.uiSchema?.enum) {
|
||||||
|
if (Array.isArray(label)) {
|
||||||
|
label = label
|
||||||
|
.map((item, index) => {
|
||||||
|
const option = targetField.uiSchema.enum.find((i) => i.value === item);
|
||||||
|
if (option) {
|
||||||
|
return (
|
||||||
|
<Tag key={index} color={option.color} style={{ marginRight: 3 }}>
|
||||||
|
{option?.label || item}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <Tag key={item}>{item}</Tag>;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.reverse();
|
||||||
|
} else {
|
||||||
|
const item = targetField.uiSchema.enum.find((i) => i.value === label);
|
||||||
|
if (item) {
|
||||||
|
label = <Tag color={item.color}>{item.label}</Tag>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetField?.type === 'date') {
|
||||||
|
label = dayjs(label).format('YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapOptions) {
|
||||||
|
return mapOptions({
|
||||||
|
[fieldNames.label]: label || EMPTY,
|
||||||
|
[fieldNames.value]: option[fieldNames.value],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
|
[fieldNames.label]: label || EMPTY,
|
||||||
|
[fieldNames.value]: option[fieldNames.value],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[targetField?.uiSchema, fieldNames],
|
||||||
|
);
|
||||||
|
const handleGetOptions = async (filter) => {
|
||||||
|
const response = await resource.list({
|
||||||
|
pageSize: 200,
|
||||||
|
params: service?.params,
|
||||||
|
filter: mergeFilter([service?.params?.filter, filter]),
|
||||||
|
tree: !filter.parentId ? true : undefined,
|
||||||
|
});
|
||||||
|
return response?.data?.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = async (value, option, index) => {
|
||||||
|
const data = await handleGetOptions({ parentId: option?.id });
|
||||||
|
const options = [...selectedOptions];
|
||||||
|
options.splice(index + 1);
|
||||||
|
if (value) {
|
||||||
|
options[index] = { ...options[index], value: option };
|
||||||
|
options[index + 1] = { key: option?.id, children: data?.length > 0 ? data : null };
|
||||||
|
}
|
||||||
|
setSelectedOptions(options);
|
||||||
|
if (['o2m', 'm2m'].includes(collectionField.interface)) {
|
||||||
|
const fieldValue = Array.isArray(associationField.fieldValue) ? associationField.fieldValue : [];
|
||||||
|
fieldValue[field.index] = option;
|
||||||
|
associationField.fieldValue = fieldValue;
|
||||||
|
} else {
|
||||||
|
associationField.value = option;
|
||||||
|
}
|
||||||
|
onChange?.(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDropdownVisibleChange = async (visible, selectedValue, index) => {
|
||||||
|
if (visible) {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await handleGetOptions({ parentId: selectedValue?.key });
|
||||||
|
setLoading(false);
|
||||||
|
setOptions(result);
|
||||||
|
if (index === selectedOptions?.length - 1 && selectedValue?.value?.id) {
|
||||||
|
const data = await handleGetOptions({ parentId: selectedValue?.value?.id });
|
||||||
|
const options = [...selectedOptions];
|
||||||
|
options.splice(index + 1);
|
||||||
|
options[index] = { ...options[index], value: selectedValue?.value };
|
||||||
|
options[index + 1] = { key: selectedValue?.value?.id, children: data?.length > 0 ? data : null };
|
||||||
|
setSelectedOptions(options);
|
||||||
|
onChange?.(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearch = async (search, selectedValue) => {
|
||||||
|
const serachParam = search
|
||||||
|
? {
|
||||||
|
[fieldNames.label]: {
|
||||||
|
[operator]: search,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
setLoading(true);
|
||||||
|
const result = await handleGetOptions({
|
||||||
|
...serachParam,
|
||||||
|
parentId: selectedValue?.key,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
setOptions(result);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Space wrap>
|
||||||
|
{selectedOptions.map((value, index) => {
|
||||||
|
return (
|
||||||
|
value.children && (
|
||||||
|
<AntdSelect
|
||||||
|
disabled={associationField.disabled}
|
||||||
|
key={`${value.value?.id}+ ${value.key} + ${fieldNames.label}`}
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
autoClearSearchValue
|
||||||
|
filterOption={false}
|
||||||
|
filterSort={null}
|
||||||
|
defaultValue={{
|
||||||
|
label: value?.value?.[fieldNames.label],
|
||||||
|
value: value?.value?.[fieldNames.value],
|
||||||
|
}}
|
||||||
|
labelInValue
|
||||||
|
onSearch={(search) => onSearch(search, value)}
|
||||||
|
fieldNames={fieldNames}
|
||||||
|
style={{ minWidth: 150 }}
|
||||||
|
onChange={((value, option) => handleSelect(value, option, index)) as any}
|
||||||
|
options={!loading ? mapOptionsToTags(options) : []}
|
||||||
|
onDropdownVisibleChange={(open) => onDropdownVisibleChange(open, value, index)}
|
||||||
|
notFoundContent={loading ? <Spin size="small" /> : null}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const AssociationCascadeSelect = connect((props: any) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CascadeSelect {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const InternalCascadeSelect = observer(
|
||||||
|
(props: any) => {
|
||||||
|
const { options: collectionField } = useAssociationFieldContext();
|
||||||
|
const selectForm = useMemo(() => createForm(), []);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const field: any = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
useEffect(() => {
|
||||||
|
const id = uid();
|
||||||
|
selectForm.addEffects(id, () => {
|
||||||
|
onFormValuesChange((form) => {
|
||||||
|
if (collectionField.interface === 'm2o') {
|
||||||
|
const value = extractLastNonNullValueObjects(form.values?.[fieldSchema.name]);
|
||||||
|
setTimeout(() => {
|
||||||
|
form.setValuesIn(fieldSchema.name, value);
|
||||||
|
props.onChange(value);
|
||||||
|
field.value = value;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const value = extractLastNonNullValueObjects(form.values?.select_array).filter(
|
||||||
|
(v) => v && Object.keys(v).length > 0,
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
field.value = value;
|
||||||
|
props.onChange(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
selectForm.removeEffects(id);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
const toValue = () => {
|
||||||
|
if (Array.isArray(field.value) && field.value.length > 0) {
|
||||||
|
return field.value;
|
||||||
|
}
|
||||||
|
return [{}];
|
||||||
|
};
|
||||||
|
const defaultValue = toValue();
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
select_array: {
|
||||||
|
type: 'array',
|
||||||
|
'x-component': 'ArrayItems',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
default: defaultValue,
|
||||||
|
items: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Space',
|
||||||
|
properties: {
|
||||||
|
sort: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'ArrayItems.SortHandle',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': AssociationCascadeSelect,
|
||||||
|
'x-component-props': {
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'ArrayItems.Remove',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
add: {
|
||||||
|
type: 'void',
|
||||||
|
title: t('Add new'),
|
||||||
|
'x-component': 'ArrayItems.Addition',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<FormProvider form={selectForm}>
|
||||||
|
{collectionField.interface === 'm2o' ? (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{ FormItem }}
|
||||||
|
schema={{
|
||||||
|
...fieldSchema,
|
||||||
|
default: field.value,
|
||||||
|
'x-component': AssociationCascadeSelect,
|
||||||
|
'x-component-props': {
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SchemaField schema={schema} />
|
||||||
|
)}
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ displayName: 'InternalCascadeSelect' },
|
||||||
|
);
|
||||||
|
|
||||||
|
function extractLastNonNullValueObjects(data, flag?) {
|
||||||
|
let result = [];
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
for (const sublist of data) {
|
||||||
|
let lastNonNullValue = null;
|
||||||
|
if (Array.isArray(sublist)) {
|
||||||
|
for (let i = sublist?.length - 1; i >= 0; i--) {
|
||||||
|
if (sublist[i].value) {
|
||||||
|
lastNonNullValue = sublist[i].value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastNonNullValue) {
|
||||||
|
result.push(lastNonNullValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (sublist?.value) {
|
||||||
|
lastNonNullValue = sublist.value;
|
||||||
|
} else {
|
||||||
|
lastNonNullValue = null;
|
||||||
|
}
|
||||||
|
if (lastNonNullValue) {
|
||||||
|
if (flag) {
|
||||||
|
result?.push?.(lastNonNullValue);
|
||||||
|
} else {
|
||||||
|
result = lastNonNullValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result?.push?.(sublist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformNestedData(inputData) {
|
||||||
|
const resultArray = [];
|
||||||
|
|
||||||
|
function recursiveTransform(data) {
|
||||||
|
if (data?.parent) {
|
||||||
|
const { parent } = data;
|
||||||
|
recursiveTransform(parent);
|
||||||
|
}
|
||||||
|
const { parent, ...other } = data;
|
||||||
|
resultArray.push(other);
|
||||||
|
}
|
||||||
|
if (inputData) {
|
||||||
|
recursiveTransform(inputData);
|
||||||
|
}
|
||||||
|
return resultArray;
|
||||||
|
}
|
@ -3,7 +3,7 @@ import { toArr } from '@formily/shared';
|
|||||||
import React, { Fragment, useRef, useState } from 'react';
|
import React, { Fragment, useRef, useState } from 'react';
|
||||||
import { useDesignable } from '../../';
|
import { useDesignable } from '../../';
|
||||||
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
|
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
|
||||||
import { CollectionProvider } from '../../../collection-manager';
|
import { CollectionProvider, useCollectionManager } from '../../../collection-manager';
|
||||||
import { RecordProvider, useRecord } from '../../../record-provider';
|
import { RecordProvider, useRecord } from '../../../record-provider';
|
||||||
import { FormProvider } from '../../core';
|
import { FormProvider } from '../../core';
|
||||||
import { useCompile } from '../../hooks';
|
import { useCompile } from '../../hooks';
|
||||||
@ -12,6 +12,7 @@ import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
|||||||
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
||||||
import schema from './schema';
|
import schema from './schema';
|
||||||
import { getLabelFormatValue, useLabelUiSchema } from './util';
|
import { getLabelFormatValue, useLabelUiSchema } from './util';
|
||||||
|
import { transformNestedData } from './InternalCascadeSelect';
|
||||||
|
|
||||||
interface IEllipsisWithTooltipRef {
|
interface IEllipsisWithTooltipRef {
|
||||||
setPopoverVisible: (boolean) => void;
|
setPopoverVisible: (boolean) => void;
|
||||||
@ -27,6 +28,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
|||||||
(props: any) => {
|
(props: any) => {
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const recordCtx = useRecord();
|
const recordCtx = useRecord();
|
||||||
|
const { getCollection } = useCollectionManager();
|
||||||
const { enableLink } = fieldSchema['x-component-props'] || {};
|
const { enableLink } = fieldSchema['x-component-props'] || {};
|
||||||
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
|
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
|
||||||
const field = useField();
|
const field = useField();
|
||||||
@ -38,10 +40,17 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
|||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { designable } = useDesignable();
|
const { designable } = useDesignable();
|
||||||
const { snapshot } = useActionContext();
|
const { snapshot } = useActionContext();
|
||||||
|
const targetCollection = getCollection(collectionField?.target);
|
||||||
|
const isTreeCollection = targetCollection.template === 'tree';
|
||||||
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
|
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
|
||||||
const renderRecords = () =>
|
const renderRecords = () =>
|
||||||
toArr(props.value).map((record, index, arr) => {
|
toArr(props.value).map((record, index, arr) => {
|
||||||
const val = toValue(compile(record?.[fieldNames?.label || 'label']), 'N/A');
|
const label = isTreeCollection
|
||||||
|
? transformNestedData(record)
|
||||||
|
.map((o) => o?.[fieldNames?.label || 'label'])
|
||||||
|
.join(' / ')
|
||||||
|
: record?.[fieldNames?.label || 'label'];
|
||||||
|
const val = toValue(compile(label), 'N/A');
|
||||||
const labelUiSchema = useLabelUiSchema(
|
const labelUiSchema = useLabelUiSchema(
|
||||||
record?.__collection || collectionField?.target,
|
record?.__collection || collectionField?.target,
|
||||||
fieldNames?.label || 'label',
|
fieldNames?.label || 'label',
|
||||||
|
@ -14,7 +14,7 @@ const ReadPrettyAssociationField = observer(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{['Select', 'Picker'].includes(currentMode) && <ReadPrettyInternalViewer {...props} />}
|
{['Select', 'Picker', 'CascadeSelect'].includes(currentMode) && <ReadPrettyInternalViewer {...props} />}
|
||||||
{currentMode === 'Tag' && <ReadPrettyInternalTag {...props} />}
|
{currentMode === 'Tag' && <ReadPrettyInternalTag {...props} />}
|
||||||
{currentMode === 'Nester' && <InternalNester {...props} />}
|
{currentMode === 'Nester' && <InternalNester {...props} />}
|
||||||
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
|
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
|
||||||
|
@ -34,6 +34,21 @@ export const useFieldModeOptions = (props?) => {
|
|||||||
!isSubTableField && { label: t('File manager'), value: 'FileManager' },
|
!isSubTableField && { label: t('File manager'), value: 'FileManager' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if (collection?.template === 'tree' && ['m2m', 'o2m', 'm2o'].includes(collectionField.interface)) {
|
||||||
|
return isReadPretty
|
||||||
|
? [
|
||||||
|
{ label: t('Title'), value: 'Select' },
|
||||||
|
{ label: t('Tag'), value: 'Tag' },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ label: t('Select'), value: 'Select' },
|
||||||
|
{ label: t('Record picker'), value: 'Picker' },
|
||||||
|
{ label: t('Sub-table'), value: 'SubTable' },
|
||||||
|
{ label: t('Cascade Select'), value: 'CascadeSelect' },
|
||||||
|
!isSubTableField && { label: t('Sub-form'), value: 'Nester' },
|
||||||
|
{ label: t('Sub-form(Popover)'), value: 'PopoverNester' },
|
||||||
|
];
|
||||||
|
}
|
||||||
switch (collectionField.interface) {
|
switch (collectionField.interface) {
|
||||||
case 'o2m':
|
case 'o2m':
|
||||||
return isReadPretty
|
return isReadPretty
|
||||||
|
Loading…
Reference in New Issue
Block a user