feat(association field):quick add new (#1953)

* refactor: association select support quieck add

* chore: tmp commit

* refactor: association select support quick add

* feat: firstOrCreate

* refactor: locale

* refactor: create api

* chore: firstOrCreate

* feat: updateOrCreate

* chore: test

* refactor: save mode edit in add new form

* feat: values to filter

* refactor: loacle improve

* refactor: loacle improve

* refactor: loacle improve

* feat: firstOrCreate http api

* refactor: code improve

* fix: build error

* refactor: local

* refactor: locale improve

* refactor: useCollectionFieldsOptions

* fix: code imprtove

* refactor: code improve

* refactor: dropdown open

* refactor: add new mode

* refactor: add new mode code improve

* refactor: add new mode code improve

* refactor: add new mode code improve

---------

Co-authored-by: chareice <chareice@live.com>
This commit is contained in:
katherinehhh 2023-06-15 16:40:42 +08:00 committed by GitHub
parent 7abfbe7be4
commit 70890f2f50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 356 additions and 54 deletions

View File

@ -143,6 +143,8 @@ export const useCreateActionProps = () => {
const currentRecord = useRecord();
const currentUserContext = useCurrentUserContext();
const currentUser = currentUserContext?.data?.data;
const action = actionField.componentProps.saveMode || 'create';
const filterKeys = actionField.componentProps.filterKeys || [];
return {
async onClick() {
const fieldNames = fields.map((field) => field.name);
@ -167,12 +169,13 @@ export const useCreateActionProps = () => {
actionField.data = field.data || {};
actionField.data.loading = true;
try {
const data = await resource.create({
const data = await resource[action]({
values: {
...values,
...overwriteValues,
...assignedValues,
},
filterKeys:filterKeys,
});
actionField.data.loading = false;
actionField.data.data = data;

View File

@ -260,6 +260,63 @@ export const useLinkageCollectionFilterOptions = (collectionName: string) => {
const options = getOptions(fields, 1);
return options;
};
// 通用
export const useCollectionFieldsOptions = (collectionName: string, maxDepth = 2, excludes = []) => {
const { getCollectionFields, getInterface } = useCollectionManager();
const fields = getCollectionFields(collectionName).filter((v) => !excludes.includes(v.interface));
const field2option = (field, depth, prefix?) => {
if (!field.interface) {
return;
}
const fieldInterface = getInterface(field.interface);
if (!fieldInterface?.filterable) {
return;
}
const { nested, children } = fieldInterface.filterable;
const value = prefix ? `${prefix}.${field.name}` : field.name;
const option = {
...field,
name: field.name,
title: field?.uiSchema?.title || field.name,
schema: field?.uiSchema,
key: value,
};
if (field.target && depth > maxDepth) {
return;
}
if (depth > maxDepth) {
return option;
}
if (children?.length) {
option['children'] = children.map((v) => {
return {
...v,
key: `${field.name}.${v.name}`,
};
});
}
if (nested) {
const targetFields = getCollectionFields(field.target).filter((v) => !excludes.includes(v.interface));
const options = getOptions(targetFields, depth + 1, field.name).filter(Boolean);
option['children'] = option['children'] || [];
option['children'].push(...options);
}
return option;
};
const getOptions = (fields, depth, prefix?) => {
const options = [];
fields.forEach((field) => {
const option = field2option(field, depth, prefix);
if (option) {
options.push(option);
}
});
return options;
};
const options = getOptions(fields, 1);
return options;
};
export const useFilterDataSource = (options) => {
const { name } = useCollection();

View File

@ -1,4 +1,4 @@
export { useCollectionFilterOptions, useSortFields, useLinkageCollectionFilterOptions } from './action-hooks';
export { useCollectionFilterOptions, useSortFields, useLinkageCollectionFilterOptions ,useCollectionFieldsOptions} from './action-hooks';
export * from './CollectionField';
export * from './CollectionFieldProvider';
export * from './CollectionManagerProvider';

View File

@ -694,5 +694,14 @@ export default {
"Duplicate mode":"Duplicate mode",
"Quick duplicate":"Quick duplicate",
"Duplicate and continue":"Duplicate and continue",
"Please configure the duplicate fields":"Please configure the duplicate fields"
"Please configure the duplicate fields":"Please configure the duplicate fields",
"Add":"Create",
"Add new mode":"Add new mode",
"Quick add":"Quick add",
"Modal add":"Modal add",
"Save mode":"Save mode",
"First or create":"First or create",
"Update or create":"Update or create",
"Find by the following fields":"Find by the following fields",
"Create":"Create"
};

View File

@ -688,4 +688,6 @@ export default {
"Sortable": "Clasificable",
"Enable link": "Activar enlace",
"Data template": "Plantilla de datos",
"Not found":"No encontrado",
"Add":"Añadir"
};

View File

@ -605,5 +605,14 @@ export default {
"Duplicate mode":"コピーモード",
"Quick duplicate":"今すぐコピー",
"Duplicate and continue":"コピーして続行",
"Please configure the duplicate fields":"コピーするフィールドを設定してください"
"Please configure the duplicate fields":"コピーするフィールドを設定してください",
"Add":"追加",
"Add new mode":"追加モード",
"Quick add":"すばやい",
"Modal add":"ポップアップ窓の追加",
"Save mode":"保存方法",
"First or create":"存在しない場合に追加",
"Update or create":"存在しなければ新規、存在すれば更新",
"Find by the following fields":"次のフィールドで検索",
"Create":"新規のみ"
}

View File

@ -668,4 +668,6 @@ export default {
'Display data template selector': 'Exibir seletor de modelo de dados',
'Form data templates': 'Modelos de dados do formulário',
"Data template": "Modelo de dados",
"Not found":"Não encontrado",
"Add":"Adicionar"
};

View File

@ -504,4 +504,6 @@ export default {
'Display data template selector': "Отображать селектор шаблона данных",
'Form data templates': "Шаблоны данных формы",
"Data template": "Шаблон данных",
"Not found":"Не найдено",
"Add":"Добавить"
}

View File

@ -771,5 +771,14 @@ export default {
"Duplicate mode":"复制方式",
"Quick duplicate":"快速复制",
"Duplicate and continue":"复制并继续",
"Please configure the duplicate fields":"请配置要复制的字段"
"Please configure the duplicate fields":"请配置要复制的字段",
"Add":"创建",
"Add new mode":"添加方式",
"Quick add":"快捷添加",
"Modal add":"弹窗添加",
'Save mode':"保存方式",
"First or create":"不存在时则新增,存在时不处理",
"Update or create":"不存在时新增,存在时更新",
"Find by the following fields":"通过以下字段查找",
"Create":"仅新增"
}

View File

@ -5,7 +5,7 @@ import { cloneDeep } from 'lodash';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDesignable } from '../..';
import { useCollection, useCollectionManager } from '../../../collection-manager';
import { useCollection, useCollectionManager, useCollectionFieldsOptions } from '../../../collection-manager';
import { OpenModeSchemaItems } from '../../../schema-items';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useLinkageAction } from './hooks';
@ -15,6 +15,7 @@ import { requestSettingsSchema } from './utils';
const Tree = connect(
AntdTree,
mapProps((props, field: any) => {
console.log(props, field);
return {
...props,
onCheck: (checkedKeys) => {
@ -57,18 +58,21 @@ export const ActionDesigner = (props) => {
const { dn } = useDesignable();
const { t } = useTranslation();
const isAction = useLinkageAction();
const isPopupAction = ['create', 'update', 'view', 'customize:popup','duplicate'].includes(fieldSchema['x-action'] || '');
const isPopupAction = ['create', 'update', 'view', 'customize:popup', 'duplicate'].includes(
fieldSchema['x-action'] || '',
);
const isUpdateModePopupAction = ['customize:bulkUpdate', 'customize:bulkEdit'].includes(fieldSchema['x-action']);
const [initialSchema, setInitialSchema] = useState<ISchema>();
const actionType = fieldSchema['x-action'] ?? '';
const isLinkageAction = linkageAction || isAction;
const isChildCollectionAction = getChildrenCollections(name).length > 0 && fieldSchema['x-action'] === 'create';
const isLink = fieldSchema['x-component'] === 'Action.Link';
const isDelete = fieldSchema?.parent?.['x-component'] === 'CollectionField';
const isDraggable = fieldSchema?.parent?.['x-component'] !== 'CollectionField';
const isDelete = fieldSchema?.parent['x-component'] === 'CollectionField';
const isDraggable = fieldSchema?.parent['x-component'] !== 'CollectionField';
const isDuplicateAction = fieldSchema['x-action'] === 'duplicate';
const { collectionList, getEnableFieldTree, onLoadData, onCheck } = useCollectionState(name);
const duplicateValues = cloneDeep(fieldSchema['x-component-props'].duplicateFields || []);
const options = useCollectionFieldsOptions(name, 1, ['id']);
useEffect(() => {
const schemaUid = uid();
const schema: ISchema = {
@ -157,6 +161,80 @@ export const ActionDesigner = (props) => {
dn.refresh();
}}
/>
{fieldSchema['x-action'] === 'submit' &&
fieldSchema.parent?.['x-initializer'] === 'CreateFormActionInitializers' && (
<SchemaSettings.ModalItem
title={t('Save mode')}
components={{ Tree }}
schema={
{
type: 'object',
title: t('Save mode'),
properties: {
saveMode: {
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
title: t('Save mode'),
default: field.componentProps.saveMode || 'create',
enum: [
{ value: 'create', label: '{{t("Create")}}' },
{ value: 'firstOrCreate', label: '{{t("First or create")}}' },
{ value: 'updateOrCreate', label: '{{t("Update or create")}}' },
],
},
filterKeys: {
type: 'array',
title: '{{ t("Find by the following fields") }}',
required: true,
default: field.componentProps.filterKeys,
'x-decorator': 'FormItem',
'x-component': 'Tree',
'x-component-props': {
treeData: options,
checkable: true,
defaultCheckedKeys: field.componentProps.filterKeys,
rootStyle: {
padding: '8px 0',
border: '1px solid #d9d9d9',
borderRadius: '2px',
maxHeight: '30vh',
overflow: 'auto',
margin: '2px 0',
},
},
'x-reactions': [
{
dependencies: ['.saveMode'],
fulfill: {
state: {
hidden: '{{ $deps[0]==="create"}}',
},
},
},
],
},
},
} as ISchema
}
onSubmit={({ saveMode, filterKeys }) => {
console.log(saveMode, filterKeys);
field.componentProps.saveMode = saveMode;
field.componentProps.filterKeys = filterKeys;
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props'].saveMode = saveMode;
fieldSchema['x-component-props'].filterKeys = filterKeys;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': {
...fieldSchema['x-component-props'],
},
},
});
dn.refresh();
}}
/>
)}
{isLinkageAction && <SchemaSettings.LinkageRules collectionName={name} />}
{isDuplicateAction && [
<SchemaSettings.ModalItem

View File

@ -1,54 +1,97 @@
import { LoadingOutlined } from '@ant-design/icons';
import { RecursionField, connect, mapProps, observer, useField, useFieldSchema } from '@formily/react';
import { Input } from 'antd';
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
import { RecursionField, connect, mapProps, observer, useField, useFieldSchema, useForm } from '@formily/react';
import { Input, Button, message } from 'antd';
import React from 'react';
import { RecordProvider } from '../../../';
import { useTranslation } from 'react-i18next';
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
import useServiceOptions from './hooks';
import useServiceOptions, { useAssociationFieldContext } from './hooks';
import { useAPIClient, useCollectionManager } from '../../../';
import { isFunction } from 'mathjs';
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
action?: string;
multiple?: boolean;
};
const InternalAssociationSelect = observer(
(props: AssociationSelectProps) => {
const { fieldNames, objectValue = true } = props;
const field: any = useField();
const fieldSchema = useFieldSchema();
const service = useServiceOptions(props);
const isAllowAddNew = fieldSchema['x-add-new'];
const value = Array.isArray(props.value) ? props.value.filter(Boolean) : props.value;
const InternalAssociationSelect = observer((props: AssociationSelectProps) => {
const { objectValue = true } = props;
const field: any = useField();
const fieldSchema = useFieldSchema();
const { getCollection } = useCollectionManager();
const service = useServiceOptions(props);
const { options: collectionField } = useAssociationFieldContext();
const value = Array.isArray(props.value) ? props.value.filter(Boolean) : props.value;
const addMode = fieldSchema['x-component-props']?.addMode;
const isAllowAddNew = fieldSchema['x-add-new'];
const { t } = useTranslation();
const { multiple } = props;
const form = useForm();
const api = useAPIClient();
const resource = api.resource(collectionField.target);
const targetCollection = getCollection(collectionField.target);
const handleCreateAction = async (props) => {
const { search: value, callBack } = props;
const {
data: { data },
} = await resource.create({
values: {
[field?.componentProps?.fieldNames?.label || 'id']: value,
},
});
if (data) {
if (['m2m', 'o2m'].includes(collectionField?.interface) && multiple !== false) {
const values = form.getValuesIn(field.path) || [];
values.push(data);
form.setValuesIn(field.path, values);
field.onInput(values);
} else {
form.setValuesIn(field.path, data);
field.onInput(data);
}
isFunction(callBack) && callBack?.();
message.success(t('Saved successfully'));
}
};
const QuickAddContent = (props) => {
return (
<div key={fieldSchema.name}>
<Input.Group compact style={{ display: 'flex', lineHeight: '32px' }}>
<RemoteSelect
style={{ width: '100%' }}
{...props}
objectValue={objectValue}
value={value}
service={service}
></RemoteSelect>
{isAllowAddNew && (
<RecordProvider record={null}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'Action';
}}
/>
</RecordProvider>
)}
</Input.Group>
<div
onClick={() => handleCreateAction(props)}
style={{ cursor: 'pointer', padding: '5px 12px', color: '#0d0c0c' }}
>
<PlusOutlined />
<span style={{ paddingLeft: 5 }}>{t('Add') + `${props.search}`}</span>
</div>
);
},
{ displayName: 'InternalAssociationSelect' },
);
};
return (
<div key={fieldSchema.name}>
<Input.Group compact style={{ display: 'flex', lineHeight: '32px' }}>
<RemoteSelect
style={{ width: '100%' }}
{...props}
objectValue={objectValue}
value={value}
service={service}
CustomDropdownRender={addMode === 'quickAdd' && QuickAddContent}
></RemoteSelect>
{(addMode === 'modalAdd' || isAllowAddNew) && (
<RecordProvider record={null}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'Action';
}}
/>
</RecordProvider>
)}
</Input.Group>
</div>
);
});
interface AssociationSelectInterface {
(props: any): React.ReactElement;

View File

@ -617,7 +617,7 @@ FormItem.Designer = function Designer() {
</div>
</SchemaSettings.Item>
)}
{!field.readPretty && isAssociationField && ['Select', 'Picker'].includes(fieldMode) && (
{!field.readPretty && isAssociationField && ['Picker'].includes(fieldMode) && (
<SchemaSettings.SwitchItem
key="allowAddNew"
title={t('Allow add new data')}
@ -658,6 +658,56 @@ FormItem.Designer = function Designer() {
}}
/>
)}
{!field.readPretty && isAssociationField && ['Select'].includes(fieldMode) && (
<SchemaSettings.SelectItem
key="add-mode"
title={t('Add new mode')}
options={[
{ label: t('None'), value: 'none' },
{ label: t('Quick add'), value: 'quickAdd' },
{ label: t('Modal add'), value: 'modalAdd' },
]}
value={field.componentProps?.addMode || 'none'}
onChange={(mode) => {
if (mode === 'modalAdd') {
const hasAddNew = fieldSchema.reduceProperties((buf, schema) => {
if (schema['x-component'] === 'Action') {
return schema;
}
return buf;
}, null);
if (!hasAddNew) {
const addNewActionschema = {
'x-action': 'create',
title: "{{t('Add new')}}",
'x-designer': 'Action.Designer',
'x-component': 'Action',
'x-decorator': 'ACLActionProvider',
'x-component-props': {
openMode: 'drawer',
type: 'default',
component: 'CreateRecordAction',
},
};
insertAdjacent('afterBegin', addNewActionschema);
}
}
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props']['addMode'] = mode;
schema['x-component-props'] = fieldSchema['x-component-props'];
field.componentProps = field.componentProps || {};
field.componentProps.addMode = mode;
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
)}
{isAssociationField && IsShowMultipleSwitch() ? (
<SchemaSettings.SwitchItem
key="multiple"

View File

@ -1,9 +1,9 @@
import { LoadingOutlined } from '@ant-design/icons';
import { connect, mapProps, mapReadPretty, useField, useFieldSchema } from '@formily/react';
import { SelectProps, Tag } from 'antd';
import { SelectProps, Tag, Empty, Divider } from 'antd';
import { uniqBy } from 'lodash';
import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ResourceActionOptions, useRequest } from '../../../api-client';
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
import { useCollection, useCollectionManager } from '../../../collection-manager';
@ -21,6 +21,7 @@ export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
mapOptions?: (data: any) => RemoteSelectProps['fieldNames'];
targetField?: any;
service: ResourceActionOptions<P>;
CustomDropdownRender?: (v: any) => any;
};
const InternalRemoteSelect = connect(
@ -34,12 +35,16 @@ const InternalRemoteSelect = connect(
manual = true,
mapOptions,
targetField: _targetField,
CustomDropdownRender,
...others
} = props;
const [open, setOpen] = useState(false);
const firstRun = useRef(false);
const fieldSchema = useFieldSchema();
const isQuickAdd = fieldSchema['x-component-props']?.addMode === 'quickAdd';
const field = useField();
const { getField } = useCollection();
const searchData = useRef(null);
const { getCollectionJoinField, getInterface } = useCollectionManager();
const collectionField = getField(fieldSchema.name);
const targetField =
@ -128,7 +133,6 @@ const InternalRemoteSelect = connect(
debounceWait: wait,
},
);
const runDep = useMemo(
() =>
JSON.stringify({
@ -137,6 +141,20 @@ const InternalRemoteSelect = connect(
}),
[service, fieldNames],
);
const CustomRenderCom = useCallback(() => {
if (searchData.current && CustomDropdownRender) {
return (
<CustomDropdownRender
search={searchData.current}
callBack={() => {
searchData.current = null;
setOpen(false);
}}
/>
);
}
return null;
}, [searchData.current]);
useEffect(() => {
// Lazy load
@ -158,6 +176,7 @@ const InternalRemoteSelect = connect(
field.componentProps?.service?.params?.filter || service?.params?.filter,
]),
});
searchData.current = search;
};
const options = useMemo(() => {
@ -167,8 +186,10 @@ const InternalRemoteSelect = connect(
const valueOptions = (value != null && (Array.isArray(value) ? value : [value])) || [];
return uniqBy(data?.data?.concat(valueOptions) || [], fieldNames.value);
}, [data?.data, value]);
const onDropdownVisibleChange = () => {
if (firstRun.current) {
const onDropdownVisibleChange = (visible) => {
setOpen(visible);
searchData.current = null;
if (firstRun.current && data?.data.length > 0) {
return;
}
run();
@ -176,6 +197,7 @@ const InternalRemoteSelect = connect(
};
return (
<Select
open={open}
dropdownMatchSelectWidth={false}
autoClearSearchValue
filterOption={false}
@ -189,6 +211,22 @@ const InternalRemoteSelect = connect(
loading={data! ? loading : true}
options={mapOptionsToTags(options)}
rawOptions={options}
dropdownRender={(menu) => {
const isFullMatch = options.some((v) => v[fieldNames.label] === searchData.current);
return (
<>
{isQuickAdd ? (
<>
{!(data?.data.length === 0 && searchData?.current) && menu}
{data?.data.length > 0 && searchData?.current && !isFullMatch && <Divider style={{ margin: 0 }} />}
{!isFullMatch && <CustomRenderCom />}
</>
) : (
menu
)}
</>
);
}}
/>
);
},