import { css } from '@emotion/css'; import { ArrayCollapse, ArrayItems, FormDialog, FormItem, FormLayout, Input } from '@formily/antd'; import { Field, GeneralField, createForm } from '@formily/core'; import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react'; import { uid } from '@formily/shared'; import { Alert, Button, Cascader, CascaderProps, Dropdown, Empty, Menu, MenuItemProps, Modal, Select, Space, Switch, } from 'antd'; import classNames from 'classnames'; import _, { cloneDeep } from 'lodash'; import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { APIClientProvider, ActionContext, CollectionFieldOptions, CollectionManagerContext, Designable, FormProvider, RemoteSchemaComponent, SchemaComponent, SchemaComponentOptions, createDesignable, findFormBlock, useAPIClient, useCollection, useCollectionManager, useCompile, useDesignable, useLinkageCollectionFilterOptions, } from '..'; import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks'; import { FilterBlockType, isSameCollection, useSupportedBlocks } from '../filter-provider/utils'; import { getTargetKey } from '../schema-component/antd/association-filter/utilts'; import { useSchemaTemplateManager } from '../schema-templates'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; import { FormDataTemplates } from './DataTemplates'; import { EnableChildCollections } from './EnableChildCollections'; import { FormLinkageRules } from './LinkageRules'; import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks'; interface SchemaSettingsProps { title?: any; dn?: Designable; field?: GeneralField; fieldSchema?: Schema; } interface SchemaSettingsContextProps { dn?: Designable; field?: GeneralField; fieldSchema?: Schema; setVisible?: any; visible?: any; template?: any; collectionName?: any; } const SchemaSettingsContext = createContext(null); export const useSchemaSettings = () => { return useContext(SchemaSettingsContext); }; interface RemoveProps { confirm?: any; removeParentsIfNoChildren?: boolean; breakRemoveOn?: ISchema | ((s: ISchema) => boolean); } type SchemaSettingsNested = { Remove?: React.FC; Item?: React.FC; Divider?: React.FC; Popup?: React.FC; SwitchItem?: React.FC; CascaderItem?: React.FC & Omit & { title: any }>; [key: string]: any; }; interface SchemaSettingsProviderProps { dn?: Designable; field?: GeneralField; fieldSchema?: Schema; setVisible?: any; visible?: any; template?: any; collectionName?: any; } export const SchemaSettingsProvider: React.FC = (props) => { const { children, fieldSchema, ...others } = props; const { getTemplateBySchema } = useSchemaTemplateManager(); const { name } = useCollection(); const template = getTemplateBySchema(fieldSchema); return ( {children} ); }; export const SchemaSettings: React.FC & SchemaSettingsNested = (props) => { const { title, dn, ...others } = props; const [visible, setVisible] = useState(false); const DropdownMenu = ( { setVisible(visible); }} overlay={{props.children}} overlayClassName={classNames( 'nb-schema-initializer-button-overlay', css` .ant-dropdown-menu-item-group-list { max-height: 40vh; overflow: auto; } `, )} > {typeof title === 'string' ? {title} : title} ); if (dn) { return ( {DropdownMenu} ); } return DropdownMenu; }; SchemaSettings.Template = function Template(props) { const { componentName, collectionName, resourceName } = props; const { t } = useTranslation(); const { getCollection } = useCollectionManager(); const { dn, setVisible, template, fieldSchema } = useSchemaSettings(); const compile = useCompile(); const api = useAPIClient(); const { dn: tdn } = useBlockTemplateContext(); const { saveAsTemplate, copyTemplateSchema } = useSchemaTemplateManager(); if (!collectionName) { return null; } if (template) { return ( { const schema = await copyTemplateSchema(template); const removed = tdn.removeWithoutEmit(); tdn.insertAfterEnd(schema, { async onSuccess() { await api.request({ url: `/uiSchemas:remove/${removed['x-uid']}`, }); }, }); }} > {t('Convert reference to duplicate')} ); } return ( { setVisible(false); const { title } = getCollection(collectionName); const values = await FormDialog(t('Save as template'), () => { return ( ); }).open({}); const sdn = createDesignable({ t, api, refresh: dn.refresh.bind(dn), current: fieldSchema.parent, }); sdn.loadAPIClientEvents(); const { key } = await saveAsTemplate({ collectionName, resourceName, componentName, name: values.name, uid: fieldSchema['x-uid'], }); sdn.removeWithoutEmit(fieldSchema); sdn.insertBeforeEnd({ type: 'void', 'x-component': 'BlockTemplate', 'x-component-props': { templateId: key, }, }); }} > {t('Save as template')} ); }; const findGridSchema = (fieldSchema) => { return fieldSchema.reduceProperties((buf, s) => { if (s['x-component'] === 'FormV2') { const f = s.reduceProperties((buf, s) => { if (s['x-component'] === 'Grid' || s['x-component'] === 'BlockTemplate') { return s; } return buf; }, null); if (f) { return f; } } return buf; }, null); }; const findBlockTemplateSchema = (fieldSchema) => { return fieldSchema.reduceProperties((buf, s) => { if (s['x-component'] === 'FormV2') { const f = s.reduceProperties((buf, s) => { if (s['x-component'] === 'BlockTemplate') { return s; } return buf; }, null); if (f) { return f; } } return buf; }, null); }; SchemaSettings.FormItemTemplate = function FormItemTemplate(props) { const { insertAdjacentPosition = 'afterBegin', componentName, collectionName, resourceName } = props; const { t } = useTranslation(); const compile = useCompile(); const { getCollection } = useCollectionManager(); const { dn, setVisible, template, fieldSchema } = useSchemaSettings(); const api = useAPIClient(); const { saveAsTemplate, copyTemplateSchema } = useSchemaTemplateManager(); if (!collectionName) { return null; } if (template) { return ( { const schema = await copyTemplateSchema(template); const templateSchema = findBlockTemplateSchema(fieldSchema); const sdn = createDesignable({ t, api, refresh: dn.refresh.bind(dn), current: templateSchema.parent, }); sdn.loadAPIClientEvents(); sdn.removeWithoutEmit(templateSchema); sdn.insertAdjacent(insertAdjacentPosition, schema, { async onSuccess() { await api.request({ url: `/uiSchemas:remove/${templateSchema['x-uid']}`, }); }, }); fieldSchema['x-template-key'] = null; await api.request({ url: `uiSchemas:patch`, method: 'post', data: { 'x-uid': fieldSchema['x-uid'], 'x-template-key': null, }, }); dn.refresh(); }} > {t('Convert reference to duplicate')} ); } return ( { setVisible(false); const { title } = getCollection(collectionName); const gridSchema = findGridSchema(fieldSchema); const values = await FormDialog(t('Save as template'), () => { const componentTitle = { FormItem: t('Form'), ReadPrettyFormItem: t('Details'), }; return ( ); }).open({}); const sdn = createDesignable({ t, api, refresh: dn.refresh.bind(dn), current: gridSchema.parent, }); sdn.loadAPIClientEvents(); const { key } = await saveAsTemplate({ collectionName, resourceName, componentName, name: values.name, uid: gridSchema['x-uid'], }); sdn.removeWithoutEmit(gridSchema); sdn.insertAdjacent(insertAdjacentPosition, { type: 'void', 'x-component': 'BlockTemplate', 'x-component-props': { templateId: key, }, }); fieldSchema['x-template-key'] = key; await api.request({ url: `uiSchemas:patch`, method: 'post', data: { 'x-uid': fieldSchema['x-uid'], 'x-template-key': key, }, }); }} > {t('Save as block template')} ); }; SchemaSettings.Item = function Item(props) { const { eventKey } = props; const key = useMemo(() => uid(), []); return ( { info.domEvent.preventDefault(); info.domEvent.stopPropagation(); props?.onClick?.(info); }} style={{ minWidth: 120 }} > {props.children || props.title} ); }; SchemaSettings.ItemGroup = (props) => { return ; }; SchemaSettings.SubMenu = (props) => { return ; }; SchemaSettings.Divider = (props) => { return ; }; SchemaSettings.Remove = function Remove(props: any) { const { confirm, removeParentsIfNoChildren, breakRemoveOn } = props; const { dn, template } = useSchemaSettings(); const { t } = useTranslation(); const field = useField(); const fieldSchema = useFieldSchema(); const ctx = useBlockTemplateContext(); const form = useForm(); return ( { Modal.confirm({ title: t('Delete block'), content: t('Are you sure you want to delete it?'), ...confirm, onOk() { const options = { removeParentsIfNoChildren, breakRemoveOn, }; if (field && field.required) { field.required = false; fieldSchema['required'] = false; } if (template && ctx?.dn) { ctx?.dn.remove(null, options); } else { dn.remove(null, options); } delete form.values[fieldSchema.name]; }, }); }} > {t('Delete')} ); }; SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: { type: FilterBlockType; emptyDescription?: string; }) { const { type, emptyDescription } = props; const fieldSchema = useFieldSchema(); const { dn } = useDesignable(); const { t } = useTranslation(); const collection = useCollection(); const dataBlocks = useSupportedBlocks(type); let { targets = [], uid } = findFilterTargets(fieldSchema); const compile = useCompile(); const Content = dataBlocks.map((block) => { const title = `${compile(block.collection.title)} #${block.uid.slice(0, 4)}`; const onHover = () => { const dom = block.dom; const designer = dom.querySelector('.general-schema-designer') as HTMLElement; if (designer) { designer.style.display = 'block'; } dom.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.2)'; dom.scrollIntoView({ behavior: 'smooth', block: 'center', }); }; const onLeave = () => { const dom = block.dom; const designer = dom.querySelector('.general-schema-designer') as HTMLElement; if (designer) { designer.style.display = null; } dom.style.boxShadow = 'none'; }; if (isSameCollection(block.collection, collection)) { return ( target.uid === block.uid)} onChange={(checked) => { if (checked) { targets.push({ uid: block.uid }); } else { targets = targets.filter((target) => target.uid !== block.uid); block.clearFilter(uid); } updateFilterTargets(fieldSchema, targets); dn.emit('patch', { schema: { ['x-uid']: uid, 'x-filter-targets': targets, }, }); dn.refresh(); }} onMouseEnter={onHover} onMouseLeave={onLeave} /> ); } const target = targets.find((target) => target.uid === block.uid); // 与筛选区块的数据表具有关系的表 return ( field.target === collection.name) .map((field) => { return { label: compile(field.uiSchema.title) || field.name, value: `${field.name}.${getTargetKey(field)}`, }; }), { label: t('Unconnected'), value: '', }, ]} onChange={(value) => { if (value === '') { targets = targets.filter((target) => target.uid !== block.uid); block.clearFilter(uid); } else { targets = targets.filter((target) => target.uid !== block.uid); targets.push({ uid: block.uid, field: value }); } updateFilterTargets(fieldSchema, targets); dn.emit('patch', { schema: { ['x-uid']: uid, 'x-filter-targets': targets, }, }); dn.refresh(); }} onClick={(e) => e.stopPropagation()} onMouseEnter={onHover} onMouseLeave={onLeave} /> ); }); return ( {Content.length ? ( Content ) : ( )} ); }; SchemaSettings.SelectItem = function SelectItem(props) { const { title, options, value, onChange, openOnHover, onClick: _onClick, ...others } = props; const [open, setOpen] = useState(false); const onClick = (...args) => { setOpen(false); _onClick?.(...args); }; const onMouseEnter = useCallback(() => setOpen(true), []); // 鼠标 hover 时,打开下拉框 const moreProps = openOnHover ? { onMouseEnter, open, } : {}; return (
{title}