import { css } from '@emotion/css'; import { ArrayCollapse, ArrayItems, FormDialog, FormItem, FormLayout, Input } from '@formily/antd'; import { createForm, Field, GeneralField } from '@formily/core'; import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react'; import { uid } from '@formily/shared'; import { error } from '@nocobase/utils/client'; import { Alert, Button, Cascader, CascaderProps, Dropdown, Empty, MenuItemProps, MenuProps, Modal, Select, Space, Switch, } from 'antd'; import classNames from 'classnames'; import _, { cloneDeep } from 'lodash'; import React, { createContext, ReactNode, useCallback, useContext, useMemo, useState, // @ts-ignore useTransition as useReactTransition, } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { ActionContextProvider, APIClientProvider, CollectionFieldOptions, CollectionManagerContext, CollectionProvider, createDesignable, Designable, findFormBlock, FormProvider, RemoteSchemaComponent, SchemaComponent, SchemaComponentContext, SchemaComponentOptions, useAPIClient, useCollection, useCollectionManager, useCompile, useDesignable, useFilterBlock, useLinkageCollectionFilterOptions, } from '..'; import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks'; import { FilterBlockType, isSameCollection, useSupportedBlocks } from '../filter-provider/utils'; import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem'; 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 { ChildDynamicComponent } from './EnableChildCollections/DynamicComponent'; import { FormLinkageRules } from './LinkageRules'; import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks'; interface SchemaSettingsProps { title?: any; dn?: Designable; field?: GeneralField; fieldSchema?: Schema; children?: ReactNode; } interface SchemaSettingsContextProps { dn?: Designable; field?: GeneralField; fieldSchema?: Schema; setVisible?: any; visible?: any; template?: any; collectionName?: any; } const SchemaSettingsContext = createContext(null); /** * 用于去除菜单的消失动画,优化操作体验 */ const hidden = css` display: none; `; 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} ); }; const overlayClassName = classNames( 'nb-schema-initializer-button-overlay', css` .ant-dropdown-menu-item-group-list { max-height: 40vh; overflow: auto; } `, ); export const SchemaSettings: React.FC & SchemaSettingsNested = (props) => { const { title, dn, ...others } = props; const [visible, setVisible] = useState(false); const { Component, getMenuItems } = useMenuItem(); const [isPending, startTransition] = useReactTransition(); const changeMenu = (v: boolean) => { startTransition(() => { setVisible(v); }); }; const items = getMenuItems(() => props.children); const dropdownMenu = () => ( <> { changeMenu(!visible); }} menu={{ items, className: classNames({ [hidden]: !visible }) }} overlayClassName={overlayClassName} > {typeof title === 'string' ? {title} : title} ); if (dn) { return ( {dropdownMenu()} ); } return dropdownMenu(); }; SchemaSettings.Template = function Template(props) { const { componentName, collectionName, resourceName, needRender } = 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 && !needRender) { 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 } = collectionName ? getCollection(collectionName) : { title: '' }; 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 { pushMenuItem } = useCollectMenuItems(); const { collectMenuItem } = useCollectMenuItem(); const { eventKey } = props; const key = useMemo(() => uid(), []); const item = { ..._.omit(props, ['children']), key, eventKey: (eventKey as any) || key, onClick: (info) => { info.domEvent.preventDefault(); info.domEvent.stopPropagation(); props?.onClick?.(info); }, style: { minWidth: 120 }, label: props.children || props.title, title: props.title, } as MenuProps['items'][0]; pushMenuItem?.(item); collectMenuItem?.(item); return null; }; SchemaSettings.ItemGroup = function ItemGroup(props) { const { Component, getMenuItems } = useMenuItem(); const { pushMenuItem } = useCollectMenuItems(); const key = useMemo(() => uid(), []); const item = { key, type: 'group', title: props.title, label: props.title, children: getMenuItems(() => props.children), } as MenuProps['items'][0]; pushMenuItem(item); return ; }; SchemaSettings.SubMenu = function SubMenu(props) { const { Component, getMenuItems } = useMenuItem(); const { pushMenuItem } = useCollectMenuItems(); const key = useMemo(() => uid(), []); const item = { key, label: props.title, title: props.title, children: getMenuItems(() => props.children), } as MenuProps['items'][0]; pushMenuItem(item); return ; }; SchemaSettings.Divider = function Divider() { const { pushMenuItem } = useCollectMenuItems(); const key = useMemo(() => uid(), []); const item = { key, type: 'divider', } as MenuProps['items'][0]; pushMenuItem(item); return null; }; 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 { inProvider } = useFilterBlock(); const dataBlocks = useSupportedBlocks(type); // eslint-disable-next-line prefer-const let { targets = [], uid } = findFilterTargets(fieldSchema); const compile = useCompile(); if (!inProvider) { return null; } 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, }, }).catch(error); 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}