From 0b6fed79d3209387b433b58458069631baee99d7 Mon Sep 17 00:00:00 2001 From: Dunqing Date: Mon, 28 Nov 2022 10:08:01 +0800 Subject: [PATCH] feat: support use select field (#1105) * feat: add RemoteSelect * feat: match * feat: support use select field * fix: revert no need code * fix: remove console * feat: reuse select * feat: support multiple value and labelInValue * fix: incorrect import * fix: missing title in RemoteSelect * feat: add association-select * fix: edit * feat: support sort * feat: support filter * fix: break * fix: loss of filter data after closing the drawer * fix: multiple not support in select * fix: some bugs in multiple * feat: support select in o2m,o2o * feat: support more association field * feat: improve * fix: remove unused files * feat: make o2m also support pattern * fix: missing value on edit --- .../client/src/api-client/hooks/useRequest.ts | 2 +- .../collection-manager/interfaces/linkTo.ts | 17 +- .../src/collection-manager/interfaces/m2m.tsx | 36 +- .../src/collection-manager/interfaces/m2o.tsx | 79 +- .../src/collection-manager/interfaces/o2m.tsx | 122 +-- .../src/collection-manager/interfaces/o2o.tsx | 108 +-- .../association-select/AssociationSelect.tsx | 692 ++++++++++++++++++ .../antd/association-select/ReadPretty.tsx | 10 + .../antd/association-select/index.md | 26 + .../antd/association-select/index.ts | 1 + .../association-select/useServiceOptions.ts | 20 + .../antd/form-item/FormItem.tsx | 102 ++- .../client/src/schema-component/antd/index.ts | 2 + .../antd/remote-select/ReadPretty.tsx | 30 + .../antd/remote-select/RemoteSelect.tsx | 89 +++ .../antd/remote-select/index.md | 26 + .../antd/remote-select/index.ts | 1 + .../antd/remote-select/shared.ts | 7 + .../schema-component/antd/select/Select.tsx | 22 +- .../src/schema-component/antd/select/index.ts | 1 + .../schema-component/antd/select/shared.ts | 1 + .../src/schema-component/hooks/index.ts | 1 + .../hooks/useFieldComponentOptions.ts | 42 ++ 23 files changed, 1201 insertions(+), 236 deletions(-) create mode 100644 packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx create mode 100644 packages/core/client/src/schema-component/antd/association-select/ReadPretty.tsx create mode 100644 packages/core/client/src/schema-component/antd/association-select/index.md create mode 100644 packages/core/client/src/schema-component/antd/association-select/index.ts create mode 100644 packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts create mode 100644 packages/core/client/src/schema-component/antd/remote-select/ReadPretty.tsx create mode 100644 packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx create mode 100644 packages/core/client/src/schema-component/antd/remote-select/index.md create mode 100644 packages/core/client/src/schema-component/antd/remote-select/index.ts create mode 100644 packages/core/client/src/schema-component/antd/remote-select/shared.ts create mode 100644 packages/core/client/src/schema-component/hooks/useFieldComponentOptions.ts diff --git a/packages/core/client/src/api-client/hooks/useRequest.ts b/packages/core/client/src/api-client/hooks/useRequest.ts index 368d47012..93c3c5f0d 100644 --- a/packages/core/client/src/api-client/hooks/useRequest.ts +++ b/packages/core/client/src/api-client/hooks/useRequest.ts @@ -10,7 +10,7 @@ import { assign } from './assign'; type FunctionService = (...args: any[]) => Promise; -type ResourceActionOptions

= { +export type ResourceActionOptions

= { resource?: string; resourceOf?: any; action?: string; diff --git a/packages/core/client/src/collection-manager/interfaces/linkTo.ts b/packages/core/client/src/collection-manager/interfaces/linkTo.ts index e75e52772..de8732852 100644 --- a/packages/core/client/src/collection-manager/interfaces/linkTo.ts +++ b/packages/core/client/src/collection-manager/interfaces/linkTo.ts @@ -47,10 +47,19 @@ export const linkTo: IField = { }, schemaInitialize(schema: ISchema, { readPretty, block }) { if (block === 'Form') { - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - selector: cloneDeep(recordPickerSelector), - }; + if (schema['x-component'] === 'AssociationSelect') { + Object.assign(schema, { + type: 'string', + 'x-designer': 'AssociationSelect.Designer', + }); + } else { + schema.type = 'string'; + schema['properties'] = { + viewer: cloneDeep(recordPickerViewer), + selector: cloneDeep(recordPickerSelector), + }; + } + return schema } else { if (readPretty) { schema['properties'] = { diff --git a/packages/core/client/src/collection-manager/interfaces/m2m.tsx b/packages/core/client/src/collection-manager/interfaces/m2m.tsx index f04c737e9..8177c4983 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2m.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2m.tsx @@ -1,7 +1,13 @@ import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; import { cloneDeep } from 'lodash'; -import { defaultProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties'; +import { + defaultProps, + recordPickerSelector, + recordPickerViewer, + relationshipType, + reverseFieldProperties, +} from './properties'; import { IField } from './types'; export const m2m: IField = { @@ -47,20 +53,28 @@ export const m2m: IField = { }, schemaInitialize(schema: ISchema, { readPretty, block }) { if (block === 'Form') { - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - selector: cloneDeep(recordPickerSelector), - }; - } else { - if (readPretty) { + if (schema['x-component'] === 'AssociationSelect') { + Object.assign(schema, { + type: 'string', + 'x-designer': 'AssociationSelect.Designer', + }); + } else { + schema.type = 'string'; schema['properties'] = { viewer: cloneDeep(recordPickerViewer), - }; - } else { - schema['properties'] = { selector: cloneDeep(recordPickerSelector), - } + }; } + return schema; + } + if (readPretty) { + schema['properties'] = { + viewer: cloneDeep(recordPickerViewer), + }; + } else { + schema['properties'] = { + selector: cloneDeep(recordPickerSelector), + }; } if (['Table', 'Kanban'].includes(block)) { schema['x-component-props'] = schema['x-component-props'] || {}; diff --git a/packages/core/client/src/collection-manager/interfaces/m2o.tsx b/packages/core/client/src/collection-manager/interfaces/m2o.tsx index 774cb8e8f..5d2d9c7cd 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2o.tsx @@ -1,6 +1,12 @@ import { ISchema } from '@formily/react'; import { cloneDeep } from 'lodash'; -import { constraintsProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties'; +import { + constraintsProps, + recordPickerSelector, + recordPickerViewer, + relationshipType, + reverseFieldProperties, +} from './properties'; import { IField } from './types'; export const m2o: IField = { @@ -44,63 +50,30 @@ export const m2o: IField = { }, }, }, - schemaInitialize(schema: ISchema, { field, block, readPretty, action }) { - if (block === 'Form' && schema['x-component'] === 'FormField') { - const association = `${field.collectionName}.${field.name}`; - schema.type = 'void'; - schema.properties = { - block: { - type: 'void', - 'x-decorator': 'FormFieldProvider', - 'x-decorator-props': { - collection: field.target, - association: association, - resource: association, - action, - fieldName: field.name, - readPretty - }, - 'x-component': 'CardItem', - 'x-component-props': { - bordered: true, - }, - properties: { - [field.name]: { - type: 'object', - 'x-component': 'FormV2', - 'x-component-props': { - useProps: '{{ useFormFieldProps }}', - }, - properties: { - __form_grid: { - type: 'void', - 'x-component': 'Grid', - 'x-initializer': 'FormItemInitializers', - properties: {}, - }, - } - }, - }, - }, - } - } else { - schema.type = 'string'; - if (block === 'Form') { + schemaInitialize(schema: ISchema, { block, readPretty }) { + if (block === 'Form') { + if (schema['x-component'] === 'AssociationSelect') { + Object.assign(schema, { + type: 'string', + 'x-designer': 'AssociationSelect.Designer', + }); + } else { + schema.type = 'string'; schema['properties'] = { viewer: cloneDeep(recordPickerViewer), selector: cloneDeep(recordPickerSelector), }; - } else { - if (readPretty) { - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - }; - } else { - schema['properties'] = { - selector: cloneDeep(recordPickerSelector), - } - } } + return schema; + } + if (readPretty) { + schema['properties'] = { + viewer: cloneDeep(recordPickerViewer), + }; + } else { + schema['properties'] = { + selector: cloneDeep(recordPickerSelector), + }; } if (['Table', 'Kanban'].includes(block)) { schema['x-component-props'] = schema['x-component-props'] || {}; diff --git a/packages/core/client/src/collection-manager/interfaces/o2m.tsx b/packages/core/client/src/collection-manager/interfaces/o2m.tsx index 627571810..32edb18ac 100644 --- a/packages/core/client/src/collection-manager/interfaces/o2m.tsx +++ b/packages/core/client/src/collection-manager/interfaces/o2m.tsx @@ -1,6 +1,12 @@ import { ISchema } from '@formily/react'; import { cloneDeep } from 'lodash'; -import { constraintsProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties'; +import { + constraintsProps, + recordPickerSelector, + recordPickerViewer, + relationshipType, + reverseFieldProperties, +} from './properties'; import { IField } from './types'; export const o2m: IField = { @@ -45,76 +51,72 @@ export const o2m: IField = { }, }, schemaInitialize(schema: ISchema, { field, block, readPretty }) { - if (block === 'Form' && schema['x-component'] === 'TableField') { - const association = `${field.collectionName}.${field.name}`; - schema['type'] = 'void'; - schema['properties'] = { - block: { - type: 'void', - 'x-decorator': 'TableFieldProvider', - 'x-decorator-props': { - collection: field.target, - association: association, - resource: association, - action: 'list', - params: { - paginate: false, + if (block === 'Form') { + if (schema['x-component'] === 'TableField') { + const association = `${field.collectionName}.${field.name}`; + schema['type'] = 'void'; + schema['properties'] = { + block: { + type: 'void', + 'x-decorator': 'TableFieldProvider', + 'x-decorator-props': { + collection: field.target, + association: association, + resource: association, + action: 'list', + params: { + paginate: false, + }, + showIndex: true, + dragSort: false, + fieldName: field.name, }, - showIndex: true, - dragSort: false, - fieldName: field.name, - }, - properties: { - actions: { - type: 'void', - 'x-initializer': 'SubTableActionInitializers', - 'x-component': 'TableField.ActionBar', - 'x-component-props': {}, - }, - [field.name]: { - type: 'array', - 'x-initializer': 'TableColumnInitializers', - 'x-component': 'TableV2', - 'x-component-props': { - rowSelection: { - type: 'checkbox', + properties: { + actions: { + type: 'void', + 'x-initializer': 'SubTableActionInitializers', + 'x-component': 'TableField.ActionBar', + 'x-component-props': {}, + }, + [field.name]: { + type: 'array', + 'x-initializer': 'TableColumnInitializers', + 'x-component': 'TableV2', + 'x-component-props': { + rowSelection: { + type: 'checkbox', + }, + useProps: '{{ useTableFieldProps }}', }, - useProps: '{{ useTableFieldProps }}', }, }, }, - }, - }; - } else { - // schema['x-component'] = 'CollectionField'; - // schema.type = 'array'; - - if (block === 'Form') { + }; + } else if (schema['x-component'] === 'AssociationSelect') { + Object.assign(schema, { + type: 'string', + 'x-designer': 'AssociationSelect.Designer', + }); + } else { + schema.type = 'string'; schema['properties'] = { viewer: cloneDeep(recordPickerViewer), selector: cloneDeep(recordPickerSelector), }; - } else { - if (readPretty) { - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - }; - } else { - schema['properties'] = { - selector: cloneDeep(recordPickerSelector), - }; - } } + return schema; } - // if (readPretty) { - // schema['properties'] = { - // viewer: cloneDeep(recordPickerViewer), - // }; - // } else { - // schema['properties'] = { - // selector: cloneDeep(recordPickerSelector), - // }; - // } + + if (readPretty) { + schema['properties'] = { + viewer: cloneDeep(recordPickerViewer), + }; + } else { + schema['properties'] = { + selector: cloneDeep(recordPickerSelector), + }; + } + if (['Table', 'Kanban'].includes(block)) { schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props']['ellipsis'] = true; diff --git a/packages/core/client/src/collection-manager/interfaces/o2o.tsx b/packages/core/client/src/collection-manager/interfaces/o2o.tsx index 78705303e..e69f5c863 100644 --- a/packages/core/client/src/collection-manager/interfaces/o2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/o2o.tsx @@ -1,65 +1,77 @@ import { ISchema } from '@formily/react'; import { cloneDeep } from 'lodash'; -import { constraintsProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties'; +import { + constraintsProps, + recordPickerSelector, + recordPickerViewer, + relationshipType, + reverseFieldProperties, +} from './properties'; import { IField } from './types'; const internalSchameInitialize = (schema: ISchema, { field, block, readPretty, action }) => { - if (block === 'Form' && schema['x-component'] === 'FormField') { - const association = `${field.collectionName}.${field.name}`; - schema.type = 'void'; - schema.properties = { - block: { - type: 'void', - 'x-decorator': 'FormFieldProvider', - 'x-decorator-props': { - collection: field.target, - association: association, - resource: association, - action: action, - fieldName: field.name, - readPretty, - }, - 'x-component': 'CardItem', - 'x-component-props': { - bordered: true, - }, - properties: { - [field.name]: { - type: 'object', - 'x-component': 'FormV2', - 'x-component-props': { - useProps: '{{ useFormFieldProps }}', - }, - properties: { - __form_grid: { - type: 'void', - 'x-component': 'Grid', - 'x-initializer': 'FormItemInitializers', - properties: {}, + if (block === 'Form') { + if (schema['x-component'] === 'FormField') { + const association = `${field.collectionName}.${field.name}`; + schema.type = 'void'; + schema.properties = { + block: { + type: 'void', + 'x-decorator': 'FormFieldProvider', + 'x-decorator-props': { + collection: field.target, + association: association, + resource: association, + action: action, + fieldName: field.name, + readPretty, + }, + 'x-component': 'CardItem', + 'x-component-props': { + bordered: true, + }, + properties: { + [field.name]: { + type: 'object', + 'x-component': 'FormV2', + 'x-component-props': { + useProps: '{{ useFormFieldProps }}', + }, + properties: { + __form_grid: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'FormItemInitializers', + properties: {}, + }, }, }, }, }, - }, - }; - } else { - schema.type = 'string'; - if (block === 'Form') { + }; + } else if (schema['x-component'] === 'AssociationSelect') { + Object.assign(schema, { + type: 'string', + 'x-designer': 'AssociationSelect.Designer', + }); + } else { + schema.type = 'string'; schema['properties'] = { viewer: cloneDeep(recordPickerViewer), selector: cloneDeep(recordPickerSelector), }; - } else { - if (readPretty) { - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - }; - } else { - schema['properties'] = { - selector: cloneDeep(recordPickerSelector), - }; - } } + return schema; + } + schema.type = 'string'; + if (readPretty) { + schema['properties'] = { + viewer: cloneDeep(recordPickerViewer), + }; + } else { + schema['properties'] = { + selector: cloneDeep(recordPickerSelector), + }; } }; diff --git a/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx b/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx new file mode 100644 index 000000000..e308e4ca3 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx @@ -0,0 +1,692 @@ +import { LoadingOutlined } from '@ant-design/icons'; +import { ArrayCollapse, ArrayItems, FormLayout } from '@formily/antd'; +import { Field } from '@formily/core'; +import { connect, ISchema, mapProps, mapReadPretty, useField, useFieldSchema } from '@formily/react'; +import { uid } from '@formily/shared'; +import _ from 'lodash'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useFormBlockContext, useFilterByTk } from '../../../block-provider'; +import { + useCollectionManager, + useCollection, + useSortFields, + useCollectionFilterOptions, +} from '../../../collection-manager'; +import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; +import { useDesignable, useCompile, useFieldComponentOptions } from '../../hooks'; +import { RemoteSelect, RemoteSelectProps } from '../remote-select'; +import { defaultFieldNames } from '../select'; +import { ReadPretty } from './ReadPretty'; +import useServiceOptions from './useServiceOptions'; + +export type AssociationSelectProps

= RemoteSelectProps

& { + action?: string; + multiple?: boolean; +}; + +const divWrap = (schema: ISchema) => { + return { + type: 'void', + 'x-component': 'div', + properties: { + [schema.name || uid()]: schema, + }, + }; +}; + +const InternalAssociationSelect = connect( + (props: AssociationSelectProps) => { + const { fieldNames, objectValue = true } = props; + const service = useServiceOptions(props); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { getField } = useCollection(); + const collectionField = getField(fieldSchema.name); + + const normalizeValues = useCallback( + (obj) => { + if (!objectValue && typeof obj === 'object') { + return obj[fieldNames.value]; + } + return obj; + }, + [objectValue, fieldNames.value], + ); + + const value = useMemo(() => { + if (props.value === undefined || props.value === null) { + return; + } + + if (Array.isArray(props.value)) { + return props.value.map(normalizeValues); + } else { + return normalizeValues(props.value); + } + }, [props.value, normalizeValues]); + + useEffect(() => { + if (!field.title) { + field.title = collectionField.uiSchema.title; + } + }, collectionField.title); + + return ; + }, + mapProps( + { + dataSource: 'options', + loading: true, + }, + (props, field) => { + return { + ...props, + fieldNames: { ...defaultFieldNames, ...props.fieldNames, ...field.componentProps.fieldNames }, + suffixIcon: field?.['loading'] || field?.['validating'] ? : props.suffixIcon, + }; + }, + ), + mapReadPretty(ReadPretty), +); + +interface RemoteSelectInterface { + (props: any): React.ReactElement; + Designer: React.FC; +} + +export const AssociationSelect = InternalAssociationSelect as unknown as RemoteSelectInterface; + +AssociationSelect.Designer = () => { + const { getCollectionFields, getInterface, getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection(); + const { form } = useFormBlockContext(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const tk = useFilterByTk(); + const {} = useCollection(); + const { dn, refresh, insertAdjacent } = useDesignable(); + const compile = useCompile(); + const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + const fieldComponentOptions = useFieldComponentOptions(); + const isSubFormAssocitionField = field.address.segments.includes('__form_grid'); + const interfaceConfig = getInterface(collectionField?.interface); + const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema); + const originalTitle = collectionField?.uiSchema?.title; + const targetFields = collectionField?.target ? getCollectionFields(collectionField.target) : []; + const initialValue = { + title: field.title === originalTitle ? undefined : field.title, + }; + const sortFields = useSortFields(collectionField.target); + const defaultSort = field.componentProps?.service?.params?.sort || []; + const defaultFilter = field.componentProps?.service?.params?.filter || {}; + const dataSource = useCollectionFilterOptions(collectionField.target); + console.log(collectionField?.target, ['CollectionField', 'AssociationSelect'].includes(fieldSchema['x-component'])); + + const sort = defaultSort?.map((item: string) => { + return item.startsWith('-') + ? { + field: item.substring(1), + direction: 'desc', + } + : { + field: item, + direction: 'asc', + }; + }); + if (!field.readPretty) { + initialValue['required'] = field.required; + } + + const options = targetFields + .filter((field) => !field?.target && field.type !== 'boolean') + .map((field) => ({ + value: field?.name, + label: compile(field?.uiSchema?.title) || field?.name, + })); + + let readOnlyMode = 'editable'; + if (fieldSchema['x-disabled'] === true) { + readOnlyMode = 'readonly'; + } + if (fieldSchema['x-read-pretty'] === true) { + readOnlyMode = 'read-pretty'; + } + + return ( + + {collectionField && ( + { + if (title) { + field.title = title; + fieldSchema.title = title; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + title: fieldSchema.title, + }, + }); + } + dn.refresh(); + }} + /> + )} + {!field.readPretty && ( + { + field.description = description; + fieldSchema.description = description; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + description: fieldSchema.description, + }, + }); + dn.refresh(); + }} + /> + )} + {field.readPretty && ( + { + field.decoratorProps.tooltip = tooltip; + fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {}; + fieldSchema['x-decorator-props']['tooltip'] = tooltip; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + dn.refresh(); + }} + /> + )} + {!field.readPretty && ( + { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + field.required = required; + fieldSchema['required'] = required; + schema['required'] = required; + dn.emit('patch', { + schema, + }); + refresh(); + }} + /> + )} + {form && !form?.readPretty && validateSchema && ( + = 3}}', + }, + }, + }, + }, + }, + }, + }, + } as ISchema + } + onSubmit={(v) => { + const rules = []; + for (const rule of v.rules) { + rules.push(_.pickBy(rule, _.identity)); + } + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + // return; + // if (['number'].includes(collectionField?.interface) && collectionField?.uiSchema?.['x-component-props']?.['stringMode'] === true) { + // rules['numberStringMode'] = true; + // } + if (['percent'].includes(collectionField?.interface)) { + for (const rule of rules) { + if (!!rule.maxValue || !!rule.minValue) { + rule['percentMode'] = true; + } + + if (rule.percentFormat) { + rule['percentFormats'] = true; + } + } + } + const concatValidator = _.concat([], collectionField?.uiSchema?.['x-validator'] || [], rules); + field.validator = concatValidator; + fieldSchema['x-validator'] = rules; + schema['x-validator'] = rules; + dn.emit('patch', { + schema, + }); + refresh(); + }} + /> + )} + {form && !form?.readPretty && collectionField?.uiSchema?.type && ( + { + const schema: ISchema = { + ['x-uid']: fieldSchema['x-uid'], + }; + if (field.value !== v.default) { + field.value = v.default; + } + fieldSchema.default = v.default; + schema.default = v.default; + dn.emit('patch', { + schema, + }); + refresh(); + }} + /> + )} + {form && !isSubFormAssocitionField && fieldComponentOptions && ( + { + const schema: ISchema = { + name: collectionField.name, + type: 'void', + required: fieldSchema['required'], + description: fieldSchema['description'], + default: fieldSchema['default'], + 'x-decorator': 'FormItem', + 'x-designer': 'FormItem.Designer', + 'x-component': type, + 'x-validator': fieldSchema['x-validator'], + 'x-collection-field': fieldSchema['x-collection-field'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + 'x-component-props': { + ...collectionField?.uiSchema?.['x-component-props'], + ...fieldSchema['x-component-props'], + }, + }; + + interfaceConfig?.schemaInitialize?.(schema, { + field: collectionField, + block: 'Form', + readPretty: field.readPretty, + action: tk ? 'get' : null, + }); + + if (type === 'CollectionField') { + schema['type'] = 'string'; + } + + insertAdjacent('beforeBegin', divWrap(schema), { + onSuccess: () => { + dn.remove(null, { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }); + }, + }); + }} + /> + )} + { + _.set(field.componentProps, 'service.params.filter', filter); + fieldSchema['x-component-props'] = field.componentProps; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': field.componentProps, + }, + }); + }} + /> + { + const sortArr = sort.map((item) => { + return item.direction === 'desc' ? `-${item.field}` : item.field; + }); + + _.set(field.componentProps, 'service.params.sort', sortArr); + fieldSchema['x-component-props'] = field.componentProps; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': field.componentProps, + }, + }); + }} + /> + {form && !form?.readPretty && fieldSchema?.['x-component-props']?.['pattern-disable'] != true && ( + { + const schema: ISchema = { + ['x-uid']: fieldSchema['x-uid'], + }; + + switch (v) { + case 'readonly': { + fieldSchema['x-read-pretty'] = false; + fieldSchema['x-disabled'] = true; + schema['x-read-pretty'] = false; + schema['x-disabled'] = true; + field.readPretty = false; + field.disabled = true; + break; + } + case 'read-pretty': { + fieldSchema['x-read-pretty'] = true; + fieldSchema['x-disabled'] = false; + schema['x-read-pretty'] = true; + schema['x-disabled'] = false; + field.readPretty = true; + break; + } + default: { + fieldSchema['x-read-pretty'] = false; + fieldSchema['x-disabled'] = false; + schema['x-read-pretty'] = false; + schema['x-disabled'] = false; + field.readPretty = false; + field.disabled = false; + break; + } + } + + dn.emit('patch', { + schema, + }); + + dn.refresh(); + }} + /> + )} + {collectionField?.target && ['CollectionField', 'AssociationSelect'].includes(fieldSchema['x-component']) && ( + { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + const fieldNames = { + ...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'], + ...field.componentProps.fieldNames, + label, + }; + field.componentProps.fieldNames = fieldNames; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['fieldNames'] = fieldNames; + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }} + /> + )} + {collectionField && } + + + ); +}; + +export default AssociationSelect; diff --git a/packages/core/client/src/schema-component/antd/association-select/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/association-select/ReadPretty.tsx new file mode 100644 index 000000000..ee944bfbe --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-select/ReadPretty.tsx @@ -0,0 +1,10 @@ +import { observer } from '@formily/react'; +import React from 'react'; +import { RemoteSelect } from '../remote-select'; +import useServiceOptions from './useServiceOptions'; + +export const ReadPretty = observer((props: any) => { + const service = useServiceOptions(props); + + return ; +}); diff --git a/packages/core/client/src/schema-component/antd/association-select/index.md b/packages/core/client/src/schema-component/antd/association-select/index.md new file mode 100644 index 000000000..87df294a1 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-select/index.md @@ -0,0 +1,26 @@ +--- +nav: + path: /client +group: + path: /schema-components +--- + +# RemoteSelect + +## Examples + +## API + +基于 Ant Design 的 [Select](https://ant.design/components/select/#API),相关扩展属性有: + +- `objectValue` 值为 object 类型 +- `fieldNames` 默认值有区别 + +```ts +export const defaultFieldNames = { + label: 'label', + value: 'value', + color: 'color', + options: 'children', +}; +``` diff --git a/packages/core/client/src/schema-component/antd/association-select/index.ts b/packages/core/client/src/schema-component/antd/association-select/index.ts new file mode 100644 index 000000000..d121d8577 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-select/index.ts @@ -0,0 +1 @@ +export * from './AssociationSelect'; diff --git a/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts b/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts new file mode 100644 index 000000000..0e00c9164 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts @@ -0,0 +1,20 @@ +import { useFieldSchema } from "@formily/react"; +import { useMemo } from "react"; +import { useCollection } from "../../../collection-manager"; + +export default function useServiceOptions(props) { + const { action = 'list', service } = props + const fieldSchema = useFieldSchema(); + const { getField } = useCollection(); + const collectionField = useMemo(() => { + return getField(fieldSchema.name); + }, [fieldSchema.name]); + + return useMemo(() => { + return { + resource: collectionField.target, + action, + ...service, + }; + }, [collectionField.target, action, service]); +} 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 ca4fa4c89..e6132b5f5 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 @@ -6,7 +6,7 @@ import { uid } from '@formily/shared'; import _ from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useCompile, useDesignable } from '../..'; +import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks'; import { useFilterByTk, useFormBlockContext } from '../../../block-provider'; import { useCollection, useCollectionManager } from '../../../collection-manager'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; @@ -41,7 +41,9 @@ export const FormItem: any = (props) => { __html: HTMLEncode(field.description).split('\n').join('
'), }} /> - ) : field.description + ) : ( + field.description + ) } /> @@ -56,13 +58,14 @@ FormItem.Designer = (props) => { const field = useField(); const fieldSchema = useFieldSchema(); const { t } = useTranslation(); - const { dn, refresh, insertAdjacent, insertBeforeBegin } = useDesignable(); + const { dn, refresh, insertAdjacent } = useDesignable(); const compile = useCompile(); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); const interfaceConfig = getInterface(collectionField?.interface); const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema); const originalTitle = collectionField?.uiSchema?.title; const targetFields = collectionField?.target ? getCollectionFields(collectionField.target) : []; + const fieldComponentOptions = useFieldComponentOptions(); const isSubFormAssocitionField = field.address.segments.includes('__form_grid'); const initialValue = { title: field.title === originalTitle ? undefined : field.title, @@ -328,62 +331,61 @@ FormItem.Designer = (props) => { )} {form && !form?.readPretty && collectionField?.uiSchema?.type && ( { + const schema: ISchema = { + ['x-uid']: fieldSchema['x-uid'], + }; + if (field.value !== v.default) { + field.value = v.default; } - } - } as ISchema} - onSubmit={(v) => { - const schema: ISchema = { - ['x-uid']: fieldSchema['x-uid'], - }; - if (field.value !== v.default) { - field.value = v.default; - } - fieldSchema.default = v.default; - schema.default = v.default; - dn.emit('patch', { - schema, - }); - refresh(); - }} - /> + fieldSchema.default = v.default; + schema.default = v.default; + dn.emit('patch', { + schema, + }); + refresh(); + }} + /> )} - {form && !isSubFormAssocitionField && ['o2o', 'oho', 'obo', 'o2m'].includes(collectionField?.interface) && ( + {form && !isSubFormAssocitionField && fieldComponentOptions && ( { + onChange={(type) => { const schema: ISchema = { name: collectionField.name, type: 'void', - // title: compile(collectionField.uiSchema?.title), + required: fieldSchema['required'], + description: fieldSchema['description'], + default: fieldSchema['default'], 'x-decorator': 'FormItem', 'x-designer': 'FormItem.Designer', - 'x-component': v, - 'x-component-props': {}, + 'x-component': type, + 'x-validator': fieldSchema['x-validator'], 'x-collection-field': fieldSchema['x-collection-field'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + 'x-component-props': { + ...collectionField?.uiSchema?.['x-component-props'], + ...fieldSchema['x-component-props'], + }, }; interfaceConfig?.schemaInitialize?.(schema, { @@ -393,10 +395,6 @@ FormItem.Designer = (props) => { action: tk ? 'get' : null, }); - if (v === 'CollectionField') { - schema['type'] = 'string'; - } - insertAdjacent('beforeBegin', divWrap(schema), { onSuccess: () => { dn.remove(null, { diff --git a/packages/core/client/src/schema-component/antd/index.ts b/packages/core/client/src/schema-component/antd/index.ts index ca5790054..12dc544e6 100644 --- a/packages/core/client/src/schema-component/antd/index.ts +++ b/packages/core/client/src/schema-component/antd/index.ts @@ -28,6 +28,8 @@ export * from './radio'; export * from './record-picker'; export * from './rich-text'; export * from './select'; +export * from './remote-select'; +export * from './association-select'; export * from './space'; export * from './table'; export * from './table-v2'; diff --git a/packages/core/client/src/schema-component/antd/remote-select/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/remote-select/ReadPretty.tsx new file mode 100644 index 000000000..3b7864458 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/remote-select/ReadPretty.tsx @@ -0,0 +1,30 @@ +import { observer, useField } from '@formily/react'; +import React from 'react'; +import { getValues } from './shared'; +import { Select, defaultFieldNames } from '../select'; +import { useRequest } from '../../../api-client'; + +export const ReadPretty = observer((props: any) => { + const fieldNames = { ...defaultFieldNames, ...props.fieldNames }; + const field = useField(); + + const { data } = useRequest( + { + action: 'list', + ...props.service, + params: { + paginate: false, + filter: { + [fieldNames.value]: { + $in: getValues(field.value, fieldNames), + }, + }, + }, + }, + { + refreshDeps: [props.service, field.value], + }, + ); + + return ; +}); 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 new file mode 100644 index 000000000..75933f69a --- /dev/null +++ b/packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx @@ -0,0 +1,89 @@ +import { LoadingOutlined } from '@ant-design/icons'; +import { connect, mapProps, mapReadPretty } from '@formily/react'; +import { SelectProps } from 'antd'; +import _ from 'lodash'; +import React from 'react'; +import { ResourceActionOptions, useRequest } from '../../../api-client'; +import { defaultFieldNames, Select } from '../select'; +import { ReadPretty } from './ReadPretty'; + +export type RemoteSelectProps

= SelectProps & { + objectValue?: boolean; + onChange?: (v: any) => void; + target: string; + wait?: number; + service: ResourceActionOptions

; +}; + +const InternalRemoteSelect = connect( + (props: RemoteSelectProps) => { + const { fieldNames, service = {}, wait = 300, ...others } = props; + + const { data, run } = useRequest( + { + action: 'list', + ...service, + params: { + pageSize: 30, + ...service?.params, + // search needs + filter: { + $and: [service?.params?.filter].filter(Boolean), + }, + }, + }, + { + debounceWait: wait, + refreshDeps: [service], + }, + ); + + const onSearch = async (search) => { + run({ + filter: { + $and: [ + { + [fieldNames.label]: { + $includes: search, + }, + }, + service?.params?.filter, + ].filter(Boolean), + }, + }); + }; + + return ( +