From 70890f2f50d2e27387b635795b46370a8d31e531 Mon Sep 17 00:00:00 2001 From: katherinehhh Date: Thu, 15 Jun 2023 16:40:42 +0800 Subject: [PATCH] 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 --- .../client/src/block-provider/hooks/index.ts | 5 +- .../src/collection-manager/action-hooks.ts | 57 +++++++++ .../client/src/collection-manager/index.tsx | 2 +- packages/core/client/src/locale/en_US.ts | 11 +- packages/core/client/src/locale/es_ES.ts | 2 + packages/core/client/src/locale/ja_JP.ts | 11 +- packages/core/client/src/locale/pt_BR.ts | 2 + packages/core/client/src/locale/ru_RU.ts | 2 + packages/core/client/src/locale/zh_CN.ts | 11 +- .../antd/action/Action.Designer.tsx | 86 ++++++++++++- .../association-field/AssociationSelect.tsx | 121 ++++++++++++------ .../antd/form-item/FormItem.tsx | 52 +++++++- .../antd/remote-select/RemoteSelect.tsx | 48 ++++++- 13 files changed, 356 insertions(+), 54 deletions(-) diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 6d8d12337..235dfb378 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -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; diff --git a/packages/core/client/src/collection-manager/action-hooks.ts b/packages/core/client/src/collection-manager/action-hooks.ts index 583116d58..2fcec9c88 100644 --- a/packages/core/client/src/collection-manager/action-hooks.ts +++ b/packages/core/client/src/collection-manager/action-hooks.ts @@ -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(); diff --git a/packages/core/client/src/collection-manager/index.tsx b/packages/core/client/src/collection-manager/index.tsx index 00c0066ad..a25e09341 100644 --- a/packages/core/client/src/collection-manager/index.tsx +++ b/packages/core/client/src/collection-manager/index.tsx @@ -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'; diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index db959ae7c..32aa77709 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -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" }; diff --git a/packages/core/client/src/locale/es_ES.ts b/packages/core/client/src/locale/es_ES.ts index 7221d646b..a4cc69671 100644 --- a/packages/core/client/src/locale/es_ES.ts +++ b/packages/core/client/src/locale/es_ES.ts @@ -688,4 +688,6 @@ export default { "Sortable": "Clasificable", "Enable link": "Activar enlace", "Data template": "Plantilla de datos", + "Not found":"No encontrado", + "Add":"Añadir" }; diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts index fc880a1d9..f8f4c28df 100644 --- a/packages/core/client/src/locale/ja_JP.ts +++ b/packages/core/client/src/locale/ja_JP.ts @@ -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":"新規のみ" } diff --git a/packages/core/client/src/locale/pt_BR.ts b/packages/core/client/src/locale/pt_BR.ts index c7e2bf5da..d4d733660 100644 --- a/packages/core/client/src/locale/pt_BR.ts +++ b/packages/core/client/src/locale/pt_BR.ts @@ -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" }; diff --git a/packages/core/client/src/locale/ru_RU.ts b/packages/core/client/src/locale/ru_RU.ts index 97fa9c52f..c5d6293a2 100644 --- a/packages/core/client/src/locale/ru_RU.ts +++ b/packages/core/client/src/locale/ru_RU.ts @@ -504,4 +504,6 @@ export default { 'Display data template selector': "Отображать селектор шаблона данных", 'Form data templates': "Шаблоны данных формы", "Data template": "Шаблон данных", + "Not found":"Не найдено", + "Add":"Добавить" } diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 38df0aeea..3abac3f3e 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -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":"仅新增" } diff --git a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx index 0c179b8d3..1b4a85600 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx @@ -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(); 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' && ( + { + 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 && } {isDuplicateAction && [ = RemoteSelectProps

& { 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 ( -

- - - - {isAllowAddNew && ( - - { - return s['x-component'] === 'Action'; - }} - /> - - )} - +
handleCreateAction(props)} + style={{ cursor: 'pointer', padding: '5px 12px', color: '#0d0c0c' }} + > + + {t('Add') + ` “${props.search}” `}
); - }, - { displayName: 'InternalAssociationSelect' }, -); + }; + return ( +
+ + + + {(addMode === 'modalAdd' || isAllowAddNew) && ( + + { + return s['x-component'] === 'Action'; + }} + /> + + )} + +
+ ); +}); interface AssociationSelectInterface { (props: any): React.ReactElement; 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 b3a9119b9..9433af29d 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 @@ -617,7 +617,7 @@ FormItem.Designer = function Designer() {
)} - {!field.readPretty && isAssociationField && ['Select', 'Picker'].includes(fieldMode) && ( + {!field.readPretty && isAssociationField && ['Picker'].includes(fieldMode) && ( )} + {!field.readPretty && isAssociationField && ['Select'].includes(fieldMode) && ( + { + 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() ? ( = SelectProps & { mapOptions?: (data: any) => RemoteSelectProps['fieldNames']; targetField?: any; service: ResourceActionOptions

; + 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 ( + { + 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 (