From 046a0b4f4d49f705d6a2c6d0cfbea704b552ee2f Mon Sep 17 00:00:00 2001 From: katherinehhh Date: Tue, 4 Jul 2023 23:16:49 +0800 Subject: [PATCH] feat(collection-manager): tableOID field and collection field (#2161) * feat: support tableOid interface * chore: child collection filter operator * chore: test * refactor: support tableOid and collection field * chore: test * fix: refactor: collectionSelect * refactor: support linkage from form in add child * refactor: add child support linkage form form * refactor: code improve * feat: support undefined value in childIn query * chore: test * refactor: locale improve * refactor: code improve * refactor: code improve * refactor: tableoid only support pg * refactor: tableoid only support pg * refactor: code improve * refactor: collection operator * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: collection field support options config * refactor: collection field support options config * feat: tableoid migration * fix: item.options?.inherits --------- Co-authored-by: ChengLei Shao Co-authored-by: chenos --- .../Configuration/AddFieldAction.tsx | 18 +++- .../Configuration/CollectionFields.tsx | 22 +++-- .../Configuration/EditFieldAction.tsx | 15 +++- .../src/collection-manager/action-hooks.ts | 63 +++++--------- .../interfaces/collection.ts | 37 +++++++++ .../collection-manager/interfaces/index.ts | 2 + .../interfaces/properties/index.ts | 11 +++ .../interfaces/properties/operators.ts | 51 ++++++++++++ .../collection-manager/interfaces/tableoid.ts | 45 ++++++++++ packages/core/client/src/locale/en_US.ts | 3 +- packages/core/client/src/locale/ja_JP.ts | 3 +- packages/core/client/src/locale/zh_CN.ts | 1 + .../antd/association-field/Nester.tsx | 5 +- .../collection-select/CollectionSelect.tsx | 31 +++++-- .../schema-component/antd/filter/Filter.tsx | 12 ++- .../antd/filter/FilterItem.tsx | 2 +- .../schema-component/antd/filter/context.ts | 1 + .../antd/filter/useFilterActionProps.ts | 83 ------------------- .../schema-component/antd/filter/useValues.ts | 4 +- .../antd/form-item/FormItem.tsx | 13 +-- .../antd/remote-select/RemoteSelect.tsx | 23 +++-- .../schema-component/common/utils/uitls.tsx | 6 +- .../components/CreateRecordAction.tsx | 73 +++++++++++----- .../DynamicComponent.tsx | 34 ++++++++ .../EnableChildCollections/index.tsx | 7 +- .../schema-settings/LinkageRules/index.tsx | 7 +- .../src/schema-settings/SchemaSettings.tsx | 52 +++++++++--- .../VariableInput/hooks/useFormVariable.ts | 4 - .../VariableInput/hooks/useVariableOptions.ts | 6 +- .../inhertits/collection-inherits.test.ts | 56 +++++++++++-- .../core/database/src/fields/uid-field.ts | 2 +- packages/core/database/src/filter-parser.ts | 1 + .../src/operators/child-collection.ts | 33 ++++++-- packages/core/database/src/repository.ts | 4 +- .../src/migrations/20230704222935-tableoid.ts | 40 +++++++++ .../src/client/components/AddFieldAction.tsx | 3 +- .../src/client/components/Entity.tsx | 4 +- 37 files changed, 535 insertions(+), 242 deletions(-) create mode 100644 packages/core/client/src/collection-manager/interfaces/collection.ts create mode 100644 packages/core/client/src/collection-manager/interfaces/tableoid.ts create mode 100644 packages/core/client/src/schema-settings/EnableChildCollections/DynamicComponent.tsx create mode 100644 packages/plugins/collection-manager/src/migrations/20230704222935-tableoid.ts diff --git a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx index 209478d73..3e24e0dcc 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx @@ -9,10 +9,10 @@ import { useTranslation } from 'react-i18next'; import { useRequest } from '../../api-client'; import { RecordProvider, useRecord } from '../../record-provider'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; -import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import { useCancelAction } from '../action-hooks'; import { useCollectionManager } from '../hooks'; import { IField } from '../interfaces/types'; +import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import * as components from './components'; import { getOptions } from './interfaces'; @@ -167,13 +167,21 @@ export const AddCollectionField = (props) => { }; export const AddFieldAction = (props) => { - const { scope, getContainer, item: record, children, trigger, align } = props; - const { getInterface, getTemplate } = useCollectionManager(); + const { scope, getContainer, item: record, children, trigger, align, database } = props; + const { getInterface, getTemplate, collections } = useCollectionManager(); const [visible, setVisible] = useState(false); const [targetScope, setTargetScope] = useState(); const [schema, setSchema] = useState({}); const compile = useCompile(); const { t } = useTranslation(); + const currentCollections = useMemo(() => { + return collections.map((v) => { + return { + label: compile(v.title), + value: v.name, + }; + }); + }, []); const getFieldOptions = useCallback(() => { const { availableFieldInterfaces } = getTemplate(record.template) || {}; const { exclude, include } = availableFieldInterfaces || {}; @@ -185,6 +193,8 @@ export const AddFieldAction = (props) => { children: v.children.filter((v) => { if (v.value === 'id') { return typeof record['autoGenId'] === 'boolean' ? record['autoGenId'] : true; + } else if (v.value === 'tableoid') { + return database?.dialect === 'postgres'; } else { return typeof record[v.value] === 'boolean' ? record[v.value] : true; } @@ -241,7 +251,6 @@ export const AddFieldAction = (props) => { }; }); }, [getFieldOptions]); - const menu = useMemo(() => { return { style: { @@ -286,6 +295,7 @@ export const AddFieldAction = (props) => { record, showReverseFieldConfig: true, targetScope, + collections: currentCollections, ...scope, }} /> diff --git a/packages/core/client/src/collection-manager/Configuration/CollectionFields.tsx b/packages/core/client/src/collection-manager/Configuration/CollectionFields.tsx index 49b86e04c..c6d911f7d 100644 --- a/packages/core/client/src/collection-manager/Configuration/CollectionFields.tsx +++ b/packages/core/client/src/collection-manager/Configuration/CollectionFields.tsx @@ -1,29 +1,30 @@ import { css } from '@emotion/css'; -import { Field, createForm } from '@formily/core'; +import { createForm, Field } from '@formily/core'; import { FieldContext, FormContext, useField } from '@formily/react'; import { Space, Switch, Table, TableColumnProps, Tag, Tooltip } from 'antd'; import React, { useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useCurrentAppInfo } from '../../appInfo'; import { RecordProvider, useRecord } from '../../record-provider'; import { Action, useAttach, useCompile } from '../../schema-component'; -import { - ResourceActionContext, - ResourceActionProvider, - useResourceActionContext, - useResourceContext, -} from '../ResourceActionProvider'; import { isDeleteButtonDisabled, useBulkDestroyActionAndRefreshCM, useDestroyActionAndRefreshCM, } from '../action-hooks'; import { useCollectionManager } from '../hooks/useCollectionManager'; +import { + ResourceActionContext, + ResourceActionProvider, + useResourceActionContext, + useResourceContext, +} from '../ResourceActionProvider'; import { AddCollectionField } from './AddFieldAction'; import { EditCollectionField } from './EditFieldAction'; import { OverridingCollectionField } from './OverridingCollectionField'; +import { collection } from './schemas/collectionFields'; import { SyncFieldsAction } from './SyncFieldsAction'; import { ViewCollectionField } from './ViewInheritedField'; -import { collection } from './schemas/collectionFields'; const indentStyle = css` .ant-table { @@ -269,6 +270,9 @@ export const CollectionFields = () => { const compile = useCompile(); const field = useField(); const { name } = useRecord(); + const { + data: { database }, + } = useCurrentAppInfo(); const { getInterface, getInheritCollections, getCollection, getCurrentCollectionFields } = useCollectionManager(); const form = useMemo(() => createForm(), []); const f = useAttach(form.createArrayField({ ...field.props, basePath: '' })); @@ -403,7 +407,7 @@ export const CollectionFields = () => { }), [t], ); - const addProps = { type: 'primary' }; + const addProps = { type: 'primary', database }; const syncProps = { type: 'primary' }; return ( diff --git a/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx index b9ae3ba19..1ca693138 100644 --- a/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx @@ -3,15 +3,15 @@ import { ISchema, useForm } from '@formily/react'; import { uid } from '@formily/shared'; import cloneDeep from 'lodash/cloneDeep'; import set from 'lodash/set'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAPIClient, useRequest } from '../../api-client'; import { RecordProvider, useRecord } from '../../record-provider'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; -import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import { useCancelAction, useUpdateAction } from '../action-hooks'; import { useCollectionManager } from '../hooks'; import { IField } from '../interfaces/types'; +import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import * as components from './components'; const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => { @@ -136,13 +136,21 @@ export const EditCollectionField = (props) => { export const EditFieldAction = (props) => { const { scope, getContainer, item: record, children } = props; - const { getInterface } = useCollectionManager(); + const { getInterface, collections } = useCollectionManager(); const [visible, setVisible] = useState(false); const [schema, setSchema] = useState({}); const api = useAPIClient(); const { t } = useTranslation(); const compile = useCompile(); const [data, setData] = useState({}); + const currentCollections = useMemo(() => { + return collections.map((v) => { + return { + label: compile(v.title), + value: v.name, + }; + }); + }, []); return ( @@ -184,6 +192,7 @@ export const EditFieldAction = (props) => { useUpdateCollectionField, useCancelAction, showReverseFieldConfig: !data?.reverseField, + collections: currentCollections, ...scope, }} /> diff --git a/packages/core/client/src/collection-manager/action-hooks.ts b/packages/core/client/src/collection-manager/action-hooks.ts index 2fcec9c88..5fdb2d41c 100644 --- a/packages/core/client/src/collection-manager/action-hooks.ts +++ b/packages/core/client/src/collection-manager/action-hooks.ts @@ -4,7 +4,6 @@ import omit from 'lodash/omit'; import { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useCollection, useCollectionManager } from '.'; -import { useCompile } from '..'; import { useRequest } from '../api-client'; import { useRecord } from '../record-provider'; import { useActionContext } from '../schema-component'; @@ -108,10 +107,28 @@ export const useChildrenCollections = (collectionName: string) => { }); }; -export const useCollectionFilterOptions = (collectionName: string) => { - const { getCollectionFields, getInterface, getChildrenCollections, getCollection } = useCollectionManager(); - const compile = useCompile(); +export const useSelfAndChildrenCollections = (collectionName: string) => { + const { getChildrenCollections, getCollection } = useCollectionManager(); + const childrenCollections = getChildrenCollections(collectionName); + const self = getCollection(collectionName); + if (!collectionName) { + return null; + } + const options = childrenCollections.map((collection: any) => { + return { + value: collection.name, + label: collection?.title || collection.name, + }; + }); + options.unshift({ + value: self.name, + label: self?.title || self.name, + }); + return options; +}; +export const useCollectionFilterOptions = (collectionName: string) => { + const { getCollectionFields, getInterface } = useCollectionManager(); return useMemo(() => { const fields = getCollectionFields(collectionName); const field2option = (field, depth) => { @@ -161,44 +178,6 @@ export const useCollectionFilterOptions = (collectionName: string) => { return options; }; const options = getOptions(fields, 1); - const collection = getCollection(collectionName); - const childrenCollections = getChildrenCollections(collectionName); - if (childrenCollections.length > 0 && !options.find((v) => v.name == 'tableoid')) { - options.push({ - name: 'tableoid', - type: 'string', - title: '{{t("Table OID(Inheritance)")}}', - schema: { - 'x-component': 'Select', - enum: [{ value: collectionName, label: compile(collection.title) }].concat( - childrenCollections.map((v) => { - return { - value: v.name, - label: compile(v.title), - }; - }), - ), - }, - operators: [ - { - label: '{{t("contains")}}', - value: '$childIn', - schema: { - 'x-component': 'Select', - 'x-component-props': { mode: 'tags' }, - }, - }, - { - label: '{{t("does not contain")}}', - value: '$childNotIn', - schema: { - 'x-component': 'Select', - 'x-component-props': { mode: 'tags' }, - }, - }, - ], - }); - } return options; }, [collectionName]); }; diff --git a/packages/core/client/src/collection-manager/interfaces/collection.ts b/packages/core/client/src/collection-manager/interfaces/collection.ts new file mode 100644 index 000000000..e19412745 --- /dev/null +++ b/packages/core/client/src/collection-manager/interfaces/collection.ts @@ -0,0 +1,37 @@ +import { ISchema } from '@formily/react'; +import { collectionDataSource, defaultProps, operators } from './properties'; +import { IField } from './types'; + +export const collection: IField = { + name: 'collection', + type: 'string', + group: 'advanced', + order: 5, + title: '{{t("Collection")}}', + sortable: true, + default: { + type: 'string', + uiSchema: { + type: 'string', + 'x-component': 'CollectionSelect', + }, + }, + availableTypes: ['string'], + hasDefaultValue: false, + properties: { + ...defaultProps, + 'uiSchema.enum': collectionDataSource, + }, + filterable: { operators: operators.collection }, + schemaInitialize(schema: ISchema, { block }) { + const props = (schema['x-component-props'] = schema['x-component-props'] || {}); + props.style = { + ...(props.style || {}), + width: '100%', + }; + + if (['Table', 'Kanban'].includes(block)) { + props['ellipsis'] = true; + } + }, +}; diff --git a/packages/core/client/src/collection-manager/interfaces/index.ts b/packages/core/client/src/collection-manager/interfaces/index.ts index 528773eb4..e434f6fad 100644 --- a/packages/core/client/src/collection-manager/interfaces/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/index.ts @@ -1,6 +1,7 @@ export * from './checkbox'; export * from './checkboxGroup'; export * from './chinaRegion'; +export * from './collection'; export * from './createdAt'; export * from './createdBy'; export * from './datetime'; @@ -25,6 +26,7 @@ export * from './radioGroup'; export * from './richText'; export * from './select'; export * from './subTable'; +export * from './tableoid'; export * from './textarea'; export * from './time'; export * from './updatedAt'; diff --git a/packages/core/client/src/collection-manager/interfaces/properties/index.ts b/packages/core/client/src/collection-manager/interfaces/properties/index.ts index 76aa6f6e7..2c8263448 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/index.ts @@ -368,3 +368,14 @@ export const recordPickerViewer = { }, }, }; + +export const collectionDataSource: ISchema = { + type: 'string', + title: '{{t("Options")}}', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-component-props': { + multiple: true, + }, + enum: '{{collections}}', +}; diff --git a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts index 51afae12a..0afd4042b 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts @@ -118,3 +118,54 @@ export const boolean = [ { label: '{{t("Yes")}}', value: '$isTruly', selected: true, noValue: true }, { label: '{{t("No")}}', value: '$isFalsy', noValue: true }, ]; + +export const tableoid = [ + { + label: '{{t("contains")}}', + value: '$childIn', + schema: { + 'x-component': 'CollectionSelect', + 'x-component-props': { mode: 'tags' }, + }, + }, + { + label: '{{t("does not contain")}}', + value: '$childNotIn', + schema: { + 'x-component': 'CollectionSelect', + 'x-component-props': { mode: 'tags' }, + }, + }, +]; + +export const collection = [ + { + label: '{{t("is")}}', + value: '$eq', + selected: true, + schema: { 'x-component': 'CollectionSelect' }, + }, + { + label: '{{t("is not")}}', + value: '$ne', + schema: { 'x-component': 'CollectionSelect' }, + }, + { + label: '{{t("contains")}}', + value: '$in', + schema: { + 'x-component': 'CollectionSelect', + 'x-component-props': { mode: 'tags' }, + }, + }, + { + label: '{{t("does not contain")}}', + value: '$notIn', + schema: { + 'x-component': 'CollectionSelect', + 'x-component-props': { mode: 'tags' }, + }, + }, + { label: '{{t("is empty")}}', value: '$empty', noValue: true }, + { label: '{{t("is not empty")}}', value: '$notEmpty', noValue: true }, +]; \ No newline at end of file diff --git a/packages/core/client/src/collection-manager/interfaces/tableoid.ts b/packages/core/client/src/collection-manager/interfaces/tableoid.ts new file mode 100644 index 000000000..987507eae --- /dev/null +++ b/packages/core/client/src/collection-manager/interfaces/tableoid.ts @@ -0,0 +1,45 @@ +import { operators } from './properties'; +import { IField } from './types'; + +export const tableoid: IField = { + name: 'tableoid', + type: 'object', + group: 'systemInfo', + order: 0, + title: '{{t("Table OID")}}', + sortable: true, + default: { + name: '__collection', + type: 'virtual', + uiSchema: { + type: 'string', + title: '{{t("Table OID")}}', + 'x-component': 'CollectionSelect', + 'x-component-props': { + isTableOid: true, + }, + 'x-read-pretty': true, + }, + }, + availableTypes: ['string'], + properties: { + 'uiSchema.title': { + type: 'string', + title: '{{t("Field display name")}}', + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'Input', + }, + name: { + type: 'string', + title: '{{t("Field name")}}', + required: true, + 'x-disabled': true, + 'x-decorator': 'FormItem', + 'x-component': 'Input', + }, + }, + filterable: { + operators: operators.tableoid, + }, +}; diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index 2b233b016..2999d925a 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -705,5 +705,6 @@ export default { "Find by the following fields":"Find by the following fields", "Create":"Create", "Current form": "Current form", - "Current object":"Current object" + "Current object":"Current object", + "Linkage form form data":"Linkage form form data", }; diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts index ff2d7a95c..dcbf53e0e 100644 --- a/packages/core/client/src/locale/ja_JP.ts +++ b/packages/core/client/src/locale/ja_JP.ts @@ -616,5 +616,6 @@ export default { "Find by the following fields":"次のフィールドで検索", "Create":"新規のみ" , "Current form":"現在のフォーム", - "Current object":"現在のオブジェクト" + "Current object":"現在のオブジェクト", + "Linkage form form data":"フォームデータから連動", } diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index cad3a8143..7d32b8794 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -790,4 +790,5 @@ export default { "File manager": "文件管理器", "Direct duplicate": "直接复制", "Copy into the form and continue to fill in": "复制到表单并继续填写", + "Linkage form form data":"从表单数据里联动", } diff --git a/packages/core/client/src/schema-component/antd/association-field/Nester.tsx b/packages/core/client/src/schema-component/antd/association-field/Nester.tsx index 19096f4a0..839a2edc8 100644 --- a/packages/core/client/src/schema-component/antd/association-field/Nester.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/Nester.tsx @@ -63,9 +63,8 @@ const ToManyNester = observer( insertCount: 1, }); field.value.splice(index + 1, 0, {}); - - each(field.form.fields, (field, key) => { - if (!field) { + each(field.form.fields, (targetField, key) => { + if (!targetField) { delete field.form.fields[key]; } }); diff --git a/packages/core/client/src/schema-component/antd/collection-select/CollectionSelect.tsx b/packages/core/client/src/schema-component/antd/collection-select/CollectionSelect.tsx index 9526db702..f8f3760c9 100644 --- a/packages/core/client/src/schema-component/antd/collection-select/CollectionSelect.tsx +++ b/packages/core/client/src/schema-component/antd/collection-select/CollectionSelect.tsx @@ -1,23 +1,39 @@ -import { connect, mapReadPretty, observer } from '@formily/react'; +import { connect, mapReadPretty, observer, useField } from '@formily/react'; import { Select, SelectProps, Tag } from 'antd'; -import React from 'react'; +import React, { useContext } from 'react'; import { useTranslation } from 'react-i18next'; -import { useCollectionManager } from '../../../collection-manager/hooks'; +import { useSelfAndChildrenCollections } from '../../../collection-manager/action-hooks'; +import { useCollection, useCollectionManager } from '../../../collection-manager/hooks'; import { useCompile } from '../../hooks'; +import { FilterContext } from '../filter/context'; export type CollectionSelectProps = SelectProps & { filter?: (item: any, index: number, array: any[]) => boolean; + isTableOid?: boolean; }; -function useOptions({ filter }: CollectionSelectProps) { +function useOptions({ filter, isTableOid }: CollectionSelectProps) { const compile = useCompile(); + const field: any = useField(); + const ctx = useContext(FilterContext); + const collection = useCollection(); + const targetCollection = isTableOid && (ctx?.collectionName || collection.name); + const inheritCollections = useSelfAndChildrenCollections(targetCollection); const { collections = [] } = useCollectionManager(); - const filtered = typeof filter === 'function' ? collections.filter(filter) : collections; + const currentCollections = field?.dataSource + ? collections.filter((v) => { + return field?.dataSource.find((i) => i.value === v.name) || field?.dataSource.includes(v.name); + }) + : collections; + const filtered = + typeof filter === 'function' + ? (inheritCollections || currentCollections).filter(filter) + : inheritCollections || currentCollections; return filtered .filter((item) => !item.hidden) .map((item) => ({ - label: compile(item.title), - value: item.name, + label: compile(item.title || item.label), + value: item.name || item.value, color: item.category?.color, })); } @@ -27,7 +43,6 @@ export const CollectionSelect = connect( const { filter, ...others } = props; const options = useOptions(props); const { t } = useTranslation(); - return (