diff --git a/packages/core/client/src/acl/ACLProvider.tsx b/packages/core/client/src/acl/ACLProvider.tsx index 690528db9..c73db8595 100644 --- a/packages/core/client/src/acl/ACLProvider.tsx +++ b/packages/core/client/src/acl/ACLProvider.tsx @@ -213,7 +213,7 @@ export const ACLActionProvider = (props) => { if (!actionPath && resource && schema['x-action']) { actionPath = `${resource}:${schema['x-action']}`; } - if (!actionPath.includes(':')) { + if (!actionPath?.includes(':')) { actionPath = `${resource}:${actionPath}`; } if (!actionPath) { diff --git a/packages/core/client/src/block-provider/FormBlockProvider.tsx b/packages/core/client/src/block-provider/FormBlockProvider.tsx index 9fe77d221..f1d75779a 100644 --- a/packages/core/client/src/block-provider/FormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FormBlockProvider.tsx @@ -3,7 +3,7 @@ import { useField } from '@formily/react'; import { Spin } from 'antd'; import isEmpty from 'lodash/isEmpty'; import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; -import { useCollectionManager } from '../collection-manager'; +import { useCollection } from '../collection-manager'; import { RecordProvider, useRecord } from '../record-provider'; import { useDesignable } from '../schema-component'; import { BlockProvider, useBlockRequestContext } from './BlockProvider'; @@ -51,14 +51,14 @@ const InternalFormBlockProvider = (props) => { export const FormBlockProvider = (props) => { const record = useRecord(); - const { __tableName } = record; - const { getInheritCollections } = useCollectionManager(); - const inheritCollections = getInheritCollections(__tableName); + const { collection } = props; + const { __collection } = record; + const currentCollection = useCollection(); const { designable } = useDesignable(); - const flag = - !designable && __tableName && !inheritCollections.includes(props.collection) && __tableName !== props.collection; + const detailFlag = (Object.keys(record).length > 0 && designable) || __collection === collection; + const createFlag = (currentCollection.name === collection && !Object.keys(record).length) || !currentCollection.name; return ( - !flag && ( + (detailFlag || createFlag) && ( diff --git a/packages/core/client/src/collection-manager/action-hooks.ts b/packages/core/client/src/collection-manager/action-hooks.ts index 5698a19f6..3fe8d3036 100644 --- a/packages/core/client/src/collection-manager/action-hooks.ts +++ b/packages/core/client/src/collection-manager/action-hooks.ts @@ -94,6 +94,17 @@ export const useSortFields = (collectionName: string) => { }); }; +export const useChildrenCollections = (collectionName: string) => { + const { getChildrenCollections } = useCollectionManager(); + const childrenCollections = getChildrenCollections(collectionName); + return childrenCollections.map((collection: any) => { + return { + value: collection.name, + label: collection?.title || collection.name, + }; + }); +}; + export const useCollectionFilterOptions = (collectionName: string) => { const { getCollectionFields, getInterface } = useCollectionManager(); const fields = getCollectionFields(collectionName); diff --git a/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts b/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts index 2ea1d5206..cba6881f6 100644 --- a/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts +++ b/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts @@ -1,6 +1,6 @@ import { clone } from '@formily/shared'; import { CascaderProps } from 'antd'; -import { reduce, unionBy, uniq } from 'lodash'; +import { reduce, unionBy, uniq, uniqBy } from 'lodash'; import { useContext } from 'react'; import { useCompile } from '../../schema-component'; import { CollectionManagerContext } from '../context'; @@ -8,7 +8,7 @@ import { CollectionFieldOptions } from '../types'; export const useCollectionManager = () => { const { refreshCM, service, interfaces, collections, templates } = useContext(CollectionManagerContext); - const compile = useCompile() + const compile = useCompile(); const getInheritedFields = (name) => { const inheritKeys = getInheritCollections(name); const inheritedFields = reduce( @@ -72,7 +72,7 @@ export const useCollectionManager = () => { children.push(v); return getChildren(collectionKey); }); - return children; + return uniqBy(children, 'key'); }; return getChildren(name); }; @@ -81,14 +81,17 @@ export const useCollectionManager = () => { return collection?.fields || []; }; - - const getCollectionFieldsOptions = (collectionName: string, type: string | string[] = 'string', opts?: { - /** - * 为 true 时允许查询所有关联字段 - * 为 Array 时仅允许查询指定的关联字段 - */ - association?: boolean | string[]; - }) => { + const getCollectionFieldsOptions = ( + collectionName: string, + type: string | string[] = 'string', + opts?: { + /** + * 为 true 时允许查询所有关联字段 + * 为 Array 时仅允许查询指定的关联字段 + */ + association?: boolean | string[]; + }, + ) => { const { association = false } = opts || {}; if (typeof type === 'string') { type = [type]; @@ -98,9 +101,10 @@ export const useCollectionManager = () => { ?.filter( (field) => field.interface && - (type.includes(field.type) || (association && field.target && field.target !== collectionName && - Array.isArray(association) ? association.includes(field.interface) : false - )), + (type.includes(field.type) || + (association && field.target && field.target !== collectionName && Array.isArray(association) + ? association.includes(field.interface) + : false)), ) ?.map((field) => { const result: CascaderProps['options'][0] = { @@ -110,13 +114,13 @@ export const useCollectionManager = () => { if (association && field.target) { result.children = getCollectionFieldsOptions(field.target, type, opts); if (!result.children?.length) { - return null + return null; } } return result; }) // 过滤 map 产生为 null 的数据 - .filter(Boolean) + .filter(Boolean); return options; }; 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 478728c4f..556a88e75 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 React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDesignable } from '../..'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; -import { useCollection } from '../../../collection-manager'; +import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useRecord } from '../../../record-provider'; import { useFormBlockContext } from '../../../block-provider/FormBlockProvider'; @@ -41,6 +41,7 @@ export const ActionDesigner = (props) => { const field = useField(); const fieldSchema = useFieldSchema(); const { name } = useCollection(); + const { getChildrenCollections } = useCollectionManager(); const { dn } = useDesignable(); const { t } = useTranslation(); const isPopupAction = ['create', 'update', 'view', 'customize:popup'].includes(fieldSchema['x-action'] || ''); @@ -48,7 +49,7 @@ export const ActionDesigner = (props) => { const [initialSchema, setInitialSchema] = useState(); const actionType = fieldSchema['x-action'] ?? ''; const isLinkageAction = Object.keys(useFormBlockContext()).length > 0 && Object.keys(useRecord()).length > 0; - + const isChildCollectionAction = getChildrenCollections(name).length > 0 && fieldSchema['x-action'] === 'create'; useEffect(() => { const schemaUid = uid(); const schema: ISchema = { @@ -379,6 +380,8 @@ export const ActionDesigner = (props) => { }} /> )} + + {isChildCollectionAction && } { const disabled = form.disabled || field.disabled; const openSize = fieldSchema?.['x-component-props']?.['openSize']; const linkageRules = fieldSchema?.['x-linkage-rules'] || []; - const { designable } = useDesignable(); + const { designable, } = useDesignable(); + const tarComponent=useComponent(component)||component; useEffect(() => { linkageRules.map((v) => { return v.actions?.map((h) => { @@ -130,7 +131,7 @@ export const Action: ComposedAction = observer((props: any) => { } } }} - component={component || Button} + component={tarComponent || Button} className={classnames(className, actionDesignerCss)} > {title || compile(fieldSchema.title)} diff --git a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx index 2315cd5d7..5e3608382 100644 --- a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx @@ -1,7 +1,7 @@ import { Schema, useFieldSchema } from '@formily/react'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { SchemaInitializer, useCollection, useCollectionManager } from '../..'; +import { SchemaInitializer, useCollection, useCollectionManager, SchemaInitializerItemOptions } from '../..'; import { gridRowColWrap } from '../utils'; const recursiveParent = (schema: Schema) => { @@ -104,27 +104,70 @@ const useRelationFields = () => { return relationFields; }; -const useInheritFields = (props) => { - const { actionInitializers } = props; - const collection = useCollection(); - const { getChildrenCollections } = useCollectionManager(); - const childrenCollections = getChildrenCollections(collection.name); - return childrenCollections.map((c) => { - return { - key: c.key, +const useDetailCollections = (props) => { + const { actionInitializers, childrenCollections, collection } = props; + const detailCollections = [ + { + key: collection.name, type: 'item', - title: c?.title || c.name, + title: collection?.title || collection.name, component: 'RecordReadPrettyFormBlockInitializer', icon: false, - targetCollection: c, + targetCollection: collection, actionInitializers, - }; - }); + }, + ].concat( + childrenCollections.map((c) => { + return { + key: c.name, + type: 'item', + title: c?.title || c.name, + component: 'RecordReadPrettyFormBlockInitializer', + icon: false, + targetCollection: c, + actionInitializers, + }; + }), + ) as SchemaInitializerItemOptions[]; + return detailCollections; +}; + +const useFormCollections = (props) => { + const { actionInitializers, childrenCollections, collection } = props; + const formCollections = [ + { + key: collection.name, + type: 'item', + title: collection?.title || collection.name, + component: 'RecordFormBlockInitializer', + icon: false, + targetCollection: collection, + actionInitializers, + }, + ].concat( + childrenCollections.map((c) => { + return { + key: c.name, + type: 'item', + title: c?.title || c.name, + component: 'RecordFormBlockInitializer', + icon: false, + targetCollection: c, + actionInitializers, + }; + }), + ) as SchemaInitializerItemOptions[]; + + return formCollections; }; export const RecordBlockInitializers = (props: any) => { const { t } = useTranslation(); const { insertPosition, component, actionInitializers } = props; + const collection = useCollection(); + const { getChildrenCollections } = useCollectionManager(); + const childrenCollections = getChildrenCollections(collection.name); + const hasChildCollection = childrenCollections?.length > 0; return ( { { type: 'itemGroup', title: '{{t("Current record blocks")}}', - children: [ - { - key: 'details', - type: 'item', - title: '{{t("Details")}}', - component: 'RecordReadPrettyFormBlockInitializer', - actionInitializers, - }, - { - key: 'form', - type: 'item', - title: '{{t("Form")}}', - component: 'RecordFormBlockInitializer', - }, - ], - }, - { - type: 'itemGroup', - title: '{{t("Children collection blocks")}}', - children: useInheritFields(props), + children: hasChildCollection + ? [ + { + key: 'details', + type: 'subMenu', + title: '{{t("Details")}}', + children: useDetailCollections({ ...props, childrenCollections, collection }), + }, + { + key: 'form', + type: 'subMenu', + title: '{{t("Form")}}', + children: useFormCollections({ ...props, childrenCollections, collection }), + }, + ] + : [ + { + key: 'details', + type: 'item', + title: '{{t("Details")}}', + component: 'RecordReadPrettyFormBlockInitializer', + actionInitializers, + }, + { + key: 'form', + type: 'item', + title: '{{t("Form")}}', + component: 'RecordFormBlockInitializer', + }, + ], }, { type: 'itemGroup', diff --git a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx new file mode 100644 index 000000000..f28db872c --- /dev/null +++ b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx @@ -0,0 +1,166 @@ +import React, { useState } from 'react'; +import { DownOutlined, PlusOutlined } from '@ant-design/icons'; +import { RecursionField, useFieldSchema, useField } from '@formily/react'; +import { Dropdown, Menu, Button } from 'antd'; +import { css } from '@emotion/css'; +import { observer } from '@formily/react'; +import { useCollectionManager, useCollection, CollectionProvider } from '../../collection-manager'; +import { ActionContext, useCompile, useActionContext } from '../../schema-component'; +import { useRecordPkValue, useACLRolesCheck } from '../../acl/ACLProvider'; + +export const actionDesignerCss = css` + position: relative; + &:hover { + .general-schema-designer { + display: block; + } + } + .general-schema-designer { + position: absolute; + z-index: 999; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: none; + background: rgba(241, 139, 98, 0.06); + border: 0; + top: 0; + bottom: 0; + left: 0; + right: 0; + pointer-events: none; + > .general-schema-designer-icons { + position: absolute; + right: 2px; + top: 2px; + line-height: 16px; + pointer-events: all; + .ant-space-item { + background-color: #f18b62; + color: #fff; + line-height: 16px; + width: 16px; + padding-left: 1px; + } + } + } +`; + +const actionAclCheck = (actionPath) => { + const { data, inResources, getResourceActionParams, getStrategyActionParams } = useACLRolesCheck(); + const recordPkValue = useRecordPkValue(); + const collection = useCollection(); + const resource = collection.resource; + const parseAction = (actionPath: string, options: any = {}) => { + const [resourceName] = actionPath.split(':'); + if (data?.allowAll) { + return {}; + } + if (inResources(resourceName)) { + return getResourceActionParams(actionPath); + } + return getStrategyActionParams(actionPath); + }; + if (!actionPath && resource) { + actionPath = `${resource}:create}`; + } + if (!actionPath?.includes(':')) { + actionPath = `${resource}:${actionPath}`; + } + if (!actionPath) { + return true; + } + const params = parseAction(actionPath, { recordPkValue }); + if (!params) { + return false; + } + return true; +}; + +export const CreateRecordAction = observer((props) => { + const [visible, setVisible] = useState(false); + const collection = useCollection(); + const fieldSchema = useFieldSchema(); + const enableChildren = fieldSchema['x-enable-children'] || []; + const field = useField(); + const { getChildrenCollections } = useCollectionManager(); + const totalChildCollections = getChildrenCollections(collection.name); + const inheritsCollections = enableChildren + .map((k) => { + const childCollection = totalChildCollections.find((j) => j.name === k.collection); + return { + ...childCollection, + title: k.title||childCollection.title, + }; + }) + .filter((v) => { + return actionAclCheck(`${v.name}:create`); + }); + const [currentCollection, setCurrentCollection] = useState(collection.name); + const ctx = useActionContext(); + const compile = useCompile(); + const menu = ( + + {inheritsCollections.map((option) => { + return ( + { + setVisible(true); + setCurrentCollection(option.name); + }} + > + {compile(option.title)} + + ); + })} + + ); + return ( +
+ + {inheritsCollections?.length > 0 ? ( + } + buttonsRender={([leftButton, rightButton]) => [ + leftButton, + React.cloneElement(rightButton as React.ReactElement, { loading: false }), + ]} + overlay={menu} + onClick={(info) => { + setVisible(true); + setCurrentCollection(collection.name); + }} + > + + {props.children} + + ) : ( + + )} + + + + +
+ ); +}); + +// export const CreateRecordAction = observer((props: any) => { +// return ( +// +// {props.children} +// +// ); +// }); diff --git a/packages/core/client/src/schema-initializer/components/index.ts b/packages/core/client/src/schema-initializer/components/index.ts index 14a252069..212606f86 100644 --- a/packages/core/client/src/schema-initializer/components/index.ts +++ b/packages/core/client/src/schema-initializer/components/index.ts @@ -1,2 +1,3 @@ export * from './assigned-field'; export * from './BulkEditField'; +export * from './CreateRecordAction' \ No newline at end of file diff --git a/packages/core/client/src/schema-initializer/items/CreateActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/CreateActionInitializer.tsx index 4778b5e63..d5a00f272 100644 --- a/packages/core/client/src/schema-initializer/items/CreateActionInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CreateActionInitializer.tsx @@ -4,14 +4,14 @@ import { ActionInitializer } from './ActionInitializer'; export const CreateActionInitializer = (props) => { const schema = { type: 'void', - title: '{{ t("Add new") }}', 'x-action': 'create', + title: "{{t('Add new')}}", 'x-designer': 'Action.Designer', 'x-component': 'Action', + 'x-decorator': 'ACLActionProvider', 'x-component-props': { - icon: 'PlusOutlined', openMode: 'drawer', - type: 'primary', + component: 'CreateRecordAction', }, properties: { drawer: { diff --git a/packages/core/client/src/schema-initializer/items/RecordFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordFormBlockInitializer.tsx index 950e15cde..e4933f9d0 100644 --- a/packages/core/client/src/schema-initializer/items/RecordFormBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordFormBlockInitializer.tsx @@ -1,55 +1,54 @@ -import React from "react"; +import React from 'react'; import { FormOutlined } from '@ant-design/icons'; -import { useBlockAssociationContext } from "../../block-provider"; -import { useCollection } from "../../collection-manager"; -import { useSchemaTemplateManager } from "../../schema-templates"; -import { SchemaInitializer } from "../SchemaInitializer"; -import { createFormBlockSchema, useRecordCollectionDataSourceItems } from "../utils"; +import { useBlockAssociationContext } from '../../block-provider'; +import { useCollection } from '../../collection-manager'; +import { useSchemaTemplateManager } from '../../schema-templates'; +import { SchemaInitializer } from '../SchemaInitializer'; +import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; export const RecordFormBlockInitializer = (props) => { - const { onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; - const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const collection = useCollection(); - const association = useBlockAssociationContext(); - console.log('RecordFormBlockInitializer', collection, association); - return ( - } - {...others} - onClick={async ({ item }) => { - if (item.template) { - const s = await getTemplateSchemaByMode(item); - if (item.template.componentName === 'FormItem') { - const blockSchema = createFormBlockSchema({ - association, - collection: collection.name, - action: 'get', - useSourceId: '{{ useSourceIdFromParentRecord }}', - useParams: '{{ useParamsFromRecord }}', - actionInitializers: 'UpdateFormActionInitializers', - template: s, - }); - if (item.mode === 'reference') { - blockSchema['x-template-key'] = item.template.key; - } - insert(blockSchema); - } else { - insert(s); + const { onCreateBlockSchema, componentType, createBlockSchema, insert, targetCollection, ...others } = props; + const { getTemplateSchemaByMode } = useSchemaTemplateManager(); + const collection = targetCollection || useCollection(); + const association = useBlockAssociationContext(); + return ( + } + {...others} + onClick={async ({ item }) => { + if (item.template) { + const s = await getTemplateSchemaByMode(item); + if (item.template.componentName === 'FormItem') { + const blockSchema = createFormBlockSchema({ + association, + collection: collection.name, + action: 'get', + useSourceId: '{{ useSourceIdFromParentRecord }}', + useParams: '{{ useParamsFromRecord }}', + actionInitializers: 'UpdateFormActionInitializers', + template: s, + }); + if (item.mode === 'reference') { + blockSchema['x-template-key'] = item.template.key; } + insert(blockSchema); } else { - insert( - createFormBlockSchema({ - association, - collection: collection.name, - action: 'get', - useSourceId: '{{ useSourceIdFromParentRecord }}', - useParams: '{{ useParamsFromRecord }}', - actionInitializers: 'UpdateFormActionInitializers', - }), - ); + insert(s); } - }} - items={useRecordCollectionDataSourceItems('FormItem')} - /> - ); - }; + } else { + insert( + createFormBlockSchema({ + association, + collection: collection.name, + action: 'get', + useSourceId: '{{ useSourceIdFromParentRecord }}', + useParams: '{{ useParamsFromRecord }}', + actionInitializers: 'UpdateFormActionInitializers', + }), + ); + } + }} + items={useRecordCollectionDataSourceItems('FormItem')} + /> + ); +}; diff --git a/packages/core/client/src/schema-settings/EnableChildCollections/index.tsx b/packages/core/client/src/schema-settings/EnableChildCollections/index.tsx new file mode 100644 index 000000000..58e7eebea --- /dev/null +++ b/packages/core/client/src/schema-settings/EnableChildCollections/index.tsx @@ -0,0 +1,106 @@ +import { observer, useForm } from '@formily/react'; +import React from 'react'; +import { action } from '@formily/reactive'; +import { SchemaComponent, useCompile } from '../../schema-component'; +import { useCollectionManager } from '../../collection-manager'; + +export const EnableChildCollections = observer((props: any) => { + const { useProps } = props; + const { defaultValues, collectionName } = useProps(); + const form = useForm(); + const compile = useCompile(); + const { getChildrenCollections } = useCollectionManager(); + const childrenCollections = getChildrenCollections(collectionName); + + const useAsyncDataSource = (service: any) => { + return (field: any, options?: any) => { + field.loading = true; + service(field, options).then( + action.bound((data: any) => { + field.dataSource = data; + field.loading = false; + if (field.initialValue) { + field.disabled = true; + } + }), + ); + }; + }; + const loadData = async (field) => { + const { childrenCollections: childCollections } = form.values?.enableChildren; + return childrenCollections + .filter((v) => { + return !childCollections.find((k) => k.collection === v.name) || field.initialValue || v.name === field.value; + }) + ?.map((collection: any) => ({ + label: compile(collection.title), + value: collection.name, + })); + }; + return ( + + ); +}); diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index 271e3ca90..5a4e19aaa 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { FormDialog, FormItem, FormLayout, Input, ArrayCollapse } from '@formily/antd'; +import { FormDialog, FormItem, FormLayout, Input, ArrayCollapse, ArrayItems } from '@formily/antd'; import { createForm, Field, GeneralField } from '@formily/core'; import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react'; import _ from 'lodash'; @@ -43,6 +43,7 @@ import { useSchemaTemplateManager } from '../schema-templates'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; import { FormLinkageRules } from './LinkageRules'; import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks'; +import { EnableChildCollections } from './EnableChildCollections'; interface SchemaSettingsProps { title?: any; @@ -770,3 +771,52 @@ SchemaSettings.LinkageRules = (props) => { /> ); }; + +SchemaSettings.EnableChildCollections = (props) => { + const { collectionName } = props; + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + const { t } = useTranslation(); + return ( + { + return { + defaultValues: fieldSchema?.['x-enable-children'], + collectionName, + }; + }, + }, + }, + }, + } as ISchema + } + onSubmit={(v) => { + const enableChildren = []; + for (const item of v.enableChildren.childrenCollections) { + enableChildren.push(_.pickBy(item, _.identity)); + } + const uid = fieldSchema['x-uid']; + const schema = { + ['x-uid']: uid, + }; + fieldSchema['x-enable-children'] = enableChildren; + schema['x-enable-children'] = enableChildren; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }} + /> + ); +};