import { css } from '@emotion/css'; import { ArrayCollapse, ArrayItems, FormItem, FormLayout, Input } from '@formily/antd-v5'; import { Field, GeneralField, createForm } 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, App, Button, Cascader, CascaderProps, Dropdown, Empty, MenuItemProps, MenuProps, Modal, ModalFuncProps, Select, Space, Switch, } from 'antd'; import _, { cloneDeep, get, set } from 'lodash'; import React, { FC, ReactNode, createContext, useCallback, useContext, useEffect, useMemo, // @ts-ignore useTransition as useReactTransition, useRef, useState, } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { Router } from 'react-router-dom'; import { APIClientProvider, ActionContextProvider, CollectionFieldOptions, CollectionManagerContext, CollectionProvider, DatePickerProvider, Designable, FormDialog, FormProvider, RemoteSchemaComponent, SchemaComponent, SchemaComponentContext, SchemaComponentOptions, createDesignable, findFormBlock, useAPIClient, useActionContext, useBlockRequestContext, useCollection, useCollectionManager, useCompile, useDesignable, useFilterBlock, useGlobalTheme, useLinkageCollectionFilterOptions, useRecord, useSchemaSettingsItem, useSortFields, } from '..'; import { BlockRequestContext, useFormBlockContext, useFormBlockType, useTableBlockContext } from '../block-provider'; import { FormActiveFieldsProvider, findFilterTargets, updateFilterTargets, useFormActiveFields, } from '../block-provider/hooks'; import { useCollectionFilterOptionsV2 } from '../collection-manager/action-hooks'; import { FilterBlockType, getSupportFieldsByAssociation, getSupportFieldsByForeignKey, isSameCollection, useSupportedBlocks, } from '../filter-provider/utils'; import { FlagProvider, useFlag } from '../flag-provider'; import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem'; import { getTargetKey } from '../schema-component/antd/association-filter/utilts'; import { DynamicComponentProps } from '../schema-component/antd/filter/DynamicComponent'; import { useSchemaTemplateManager } from '../schema-templates'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; import { useLocalVariables, useVariables } from '../variables'; import { isVariable } from '../variables/utils/isVariable'; import { FormDataTemplates } from './DataTemplates'; import { DateFormatCom, ExpiresRadio } from './DateFormat/ExpiresRadio'; import { EnableChildCollections } from './EnableChildCollections'; import { ChildDynamicComponent } from './EnableChildCollections/DynamicComponent'; import { FormLinkageRules } from './LinkageRules'; import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks'; import { VariableInput, getShouldChange } from './VariableInput/VariableInput'; import { BaseVariableProvider, IsDisabledParams } from './VariableInput/hooks/useBaseVariable'; import { Option } from './VariableInput/type'; import { formatVariableScop } from './VariableInput/utils/formatVariableScop'; import { DataScopeProps } from './types'; export 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; designer?: T; } const SchemaSettingsContext = createContext(null); export function useSchemaSettings() { return useContext(SchemaSettingsContext) as SchemaSettingsContextProps; } interface SchemaSettingsProviderProps { dn?: Designable; field?: GeneralField; fieldSchema?: Schema; setVisible?: any; visible?: any; template?: any; collectionName?: any; designer?: 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 SchemaSettingsDropdown: React.FC = (props) => { const { title, dn, ...others } = props; const [visible, setVisible] = useState(false); const { Component, getMenuItems } = useMenuItem(); const [, startTransition] = useReactTransition(); const changeMenu = (v: boolean) => { // 当鼠标快速滑过时,终止菜单的渲染,防止卡顿 startTransition(() => { setVisible(v); }); }; const items = getMenuItems(() => props.children); return ( { changeMenu(open); }} overlayClassName={css` .ant-dropdown-menu-item-group-list { max-height: 300px; overflow-y: auto; } `} menu={{ items }} >
{typeof title === 'string' ? {title} : title}
); }; export const SchemaSettingsTemplate = 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(); const { theme } = useGlobalTheme(); 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 ( ); }, theme, ).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); }; export const SchemaSettingsFormItemTemplate = 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(); const { theme } = useGlobalTheme(); 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 ( ); }, theme, ).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')} ); }; export interface SchemaSettingsItemProps extends Omit { title: string; } export const SchemaSettingsItem: FC = (props) => { const { pushMenuItem } = useCollectMenuItems(); const { collectMenuItem } = useCollectMenuItem(); const { eventKey, title } = props; const { name } = useSchemaSettingsItem(); if (process.env.NODE_ENV !== 'production' && !title) { throw new Error('SchemaSettingsItem must have a title'); } const item = { key: title, ..._.omit(props, ['children', 'name']), eventKey: eventKey as any, 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; }; export interface SchemaSettingsItemGroupProps { title: string; children: any[]; } export const SchemaSettingsItemGroup: FC = (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 ; }; export interface SchemaSettingsSubMenuProps { title: string; eventKey?: string; children: any; } export const SchemaSettingsSubMenu = function SubMenu(props: SchemaSettingsSubMenuProps) { 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 ; }; export const SchemaSettingsDivider = function Divider() { const { pushMenuItem } = useCollectMenuItems(); const key = useMemo(() => uid(), []); const item = { key, type: 'divider', } as MenuProps['items'][0]; pushMenuItem(item); return null; }; export interface SchemaSettingsRemoveProps { confirm?: ModalFuncProps; removeParentsIfNoChildren?: boolean; breakRemoveOn?: ISchema | ((s: ISchema) => boolean); } export const SchemaSettingsRemove: FC = (props) => { const { confirm, removeParentsIfNoChildren, breakRemoveOn } = props; const { dn, template } = useSchemaSettings(); const { t } = useTranslation(); const field = useField(); const fieldSchema = useFieldSchema(); const ctx = useBlockTemplateContext(); const form = useForm(); const { modal } = App.useApp(); const { removeActiveFieldName } = useFormActiveFields() || {}; return ( { modal.confirm({ title: t('Delete block'), content: t('Are you sure you want to delete it?'), ...confirm, async onOk() { const options = { removeParentsIfNoChildren, breakRemoveOn, }; if (field?.required) { field.required = false; fieldSchema['required'] = false; } if (template && ctx?.dn) { await ctx?.dn.remove(null, options); } else { await dn.remove(null, options); } await confirm?.onOk?.(); delete form.values[fieldSchema.name]; removeActiveFieldName?.(fieldSchema.name as string); if (field?.setInitialValue && field?.reset) { field.setInitialValue(null); field.reset(); } }, }); }} > {t('Delete')} ); }; interface SchemaSettingsConnectDataBlocksProps { type: FilterBlockType; emptyDescription?: string; } export const SchemaSettingsConnectDataBlocks: FC = (props) => { 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(); const { getAllCollectionsInheritChain } = useCollectionManager(); 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 ( { return { label: compile(field.uiSchema.title) || field.name, value: `${field.name}.${getTargetKey(field)}`, }; }), ...getSupportFieldsByForeignKey(collection, block).map((field) => { return { label: `${compile(field.uiSchema.title) || field.name} [${t('Foreign key')}]`, value: field.name, }; }), { 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(); }} onMouseEnter={onHover} onMouseLeave={onLeave} /> ); }); return ( {Content.length ? ( Content ) : ( )} ); }; export interface SchemaSettingsSelectItemProps extends Omit, Omit { value?: SelectWithTitleProps['defaultValue']; } export const SchemaSettingsSelectItem: FC = (props) => { const { title, options, value, onChange, ...others } = props; return ( ); }; export type SchemaSettingsCascaderItemProps = CascaderProps & Omit & { title: any }; export const SchemaSettingsCascaderItem: FC = (props) => { const { title, options, value, onChange, ...others } = props; return (
{title}
); }; export interface SchemaSettingsSwitchItemProps extends Omit { title: string; checked?: boolean; onChange?: (v: boolean) => void; } export const SchemaSettingsSwitchItem: FC = (props) => { const { title, onChange, ...others } = props; const [checked, setChecked] = useState(!!props.checked); return ( { onChange?.(!checked); setChecked(!checked); }} >
{title}
); }; export interface SchemaSettingsPopupProps extends SchemaSettingsItemProps { schema?: ISchema; } export const SchemaSettingsPopupItem: FC = (props) => { const { schema, ...others } = props; const [visible, setVisible] = useState(false); const ctx = useContext(SchemaSettingsContext); return ( { // actx.setVisible(false); ctx.setVisible(false); setVisible(true); }} > {props.children || props.title} ); }; export interface SchemaSettingsActionModalItemProps extends SchemaSettingsModalItemProps, Omit { uid?: string; initialSchema?: ISchema; schema?: ISchema; beforeOpen?: () => void; maskClosable?: boolean; } export const SchemaSettingsActionModalItem: FC = React.memo((props) => { const { title, onSubmit, initialValues, beforeOpen, initialSchema, schema, modalTip, components, scope, ...others } = props; const [visible, setVisible] = useState(false); const [schemaUid, setSchemaUid] = useState(props.uid); const { t } = useTranslation(); const fieldSchema = useFieldSchema(); const ctx = useContext(SchemaSettingsContext); const { dn } = useSchemaSettings(); const compile = useCompile(); const api = useAPIClient(); const upLevelActiveFields = useFormActiveFields(); const form = useMemo( () => createForm({ initialValues: cloneDeep(initialValues), values: cloneDeep(initialValues), }), [initialValues], ); useEffect(() => { form.setInitialValues(cloneDeep(initialValues)); }, [JSON.stringify(initialValues || {})]); const cancelHandler = useCallback(() => { setVisible(false); form.reset(); }, [form]); const submitHandler = useCallback(async () => { await form.submit(); onSubmit?.(cloneDeep(form.values)); setVisible(false); }, [form, onSubmit]); const openAssignedFieldValueHandler = useCallback(async () => { if (!schemaUid && initialSchema?.['x-uid']) { fieldSchema['x-action-settings'].schemaUid = initialSchema['x-uid']; dn.emit('patch', { schema: fieldSchema }); await api.resource('uiSchemas').insert({ values: initialSchema }); setSchemaUid(initialSchema['x-uid']); } if (typeof beforeOpen === 'function') { beforeOpen?.(); } ctx.setVisible(false); setVisible(true); }, [api, ctx, dn, fieldSchema, initialSchema, schemaUid]); const onKeyDown = useCallback((e: React.KeyboardEvent): void => e.stopPropagation(), []); return ( <> {props.children || props.title} {createPortal( } > {modalTip && } {modalTip &&
} {visible && schemaUid && ( )} {visible && schema && }
, document.body, )} ); }); SchemaSettingsActionModalItem.displayName = 'SchemaSettingsActionModalItem'; export interface SchemaSettingsModalItemProps { title: string; onSubmit: (values: any) => void; initialValues?: any; schema?: ISchema | (() => ISchema); modalTip?: string; components?: any; hidden?: boolean; scope?: any; effects?: any; width?: string | number; children?: ReactNode; asyncGetInitialValues?: () => Promise; eventKey?: string; hide?: boolean; } export const SchemaSettingsModalItem: FC = (props) => { const { hidden, title, components, scope, effects, onSubmit, asyncGetInitialValues, initialValues, width = 'fit-content', ...others } = props; const options = useContext(SchemaOptionsContext); const cm = useContext(CollectionManagerContext); const collection = useCollection(); const apiClient = useAPIClient(); const { theme } = useGlobalTheme(); const ctx = useContext(BlockRequestContext); const upLevelActiveFields = useFormActiveFields(); if (hidden) { return null; } return ( { const values = asyncGetInitialValues ? await asyncGetInitialValues() : initialValues; const schema = _.isFunction(props.schema) ? props.schema() : props.schema; FormDialog( { title: schema.title || title, width }, () => { return ( 576px @media (min-width: 576px) { min-width: 520px; } // screen <= 576px @media (max-width: 576px) { min-width: 320px; } `} > ); }, theme, ) .open({ initialValues: values, effects, }) .then((values) => { onSubmit(values); return values; }) .catch((err) => { console.error(err); }); }} > {props.children || props.title} ); }; export const SchemaSettingsBlockTitleItem = function BlockTitleItem() { const field = useField(); const fieldSchema = useFieldSchema(); const { dn } = useDesignable(); const { t } = useTranslation(); return ( { const componentProps = fieldSchema['x-component-props'] || {}; componentProps.title = title; fieldSchema['x-component-props'] = componentProps; field.componentProps.title = title; dn.emit('patch', { schema: { ['x-uid']: fieldSchema['x-uid'], 'x-component-props': fieldSchema['x-component-props'], }, }); dn.refresh(); }} /> ); }; export const SchemaSettingsDefaultSortingRules = function DefaultSortingRules(props) { const { path = 'x-component-props.params.sort' } = props; const { t } = useTranslation(); const { dn } = useDesignable(); const fieldSchema = useFieldSchema(); const field = useField(); const title = props.title || t('Set default sorting rules'); const { name } = useCollection(); const defaultSort = get(fieldSchema, path) || []; const sort = defaultSort?.map((item: string) => { return item.startsWith('-') ? { field: item.substring(1), direction: 'desc', } : { field: item, direction: 'asc', }; }); const sortFields = useSortFields(props.name || name); const onSubmit = async ({ sort }) => { if (props?.onSubmit) { return props.onSubmit({ sort }); } const value = sort.map((item) => { return item.direction === 'desc' ? `-${item.field}` : item.field; }); set( field, path.replace('x-component-props', 'componentProps').replace('x-decorator-props', 'decoratorProps'), value, ); set(fieldSchema, path, value); await dn.emit('patch', { schema: fieldSchema, }); return props.onSubmitAfter?.(); }; return ( ); }; export const SchemaSettingsLinkageRules = function LinkageRules(props) { const { collectionName } = props; const fieldSchema = useFieldSchema(); const { form } = useFormBlockContext(); const { dn } = useDesignable(); const { t } = useTranslation(); const { getTemplateById } = useSchemaTemplateManager(); const variables = useVariables(); const localVariables = useLocalVariables(); const record = useRecord(); const { type: formBlockType } = useFormBlockType(); const type = props?.type || ['Action', 'Action.Link'].includes(fieldSchema['x-component']) ? 'button' : 'field'; const gridSchema = findGridSchema(fieldSchema) || fieldSchema; const schema = useMemo( () => ({ type: 'object', title: t('Linkage rules'), properties: { fieldReaction: { 'x-component': FormLinkageRules, 'x-component-props': { useProps: () => { const options = useLinkageCollectionFilterOptions(collectionName); return { options, defaultValues: gridSchema?.['x-linkage-rules'] || fieldSchema?.['x-linkage-rules'], type, linkageOptions: useLinkageCollectionFieldOptions(collectionName), collectionName, form, variables, localVariables, record, formBlockType, }; }, }, }, }, }), [collectionName, fieldSchema, form, gridSchema, localVariables, record, t, type, variables], ); const components = useMemo(() => ({ ArrayCollapse, FormLayout }), []); const onSubmit = useCallback( (v) => { const rules = []; for (const rule of v.fieldReaction.rules) { rules.push(_.pickBy(rule, _.identity)); } const templateId = gridSchema['x-component'] === 'BlockTemplate' && gridSchema['x-component-props'].templateId; const uid = (templateId && getTemplateById(templateId).uid) || gridSchema['x-uid']; const schema = { ['x-uid']: uid, }; gridSchema['x-linkage-rules'] = rules; schema['x-linkage-rules'] = rules; dn.emit('patch', { schema, }); dn.refresh(); }, [dn, getTemplateById, gridSchema], ); return ( ); }; export const useDataTemplates = (schema?: Schema) => { const fieldSchema = useFieldSchema(); if (schema) { return { templateData: _.cloneDeep(schema['x-data-templates']), }; } const formSchema = findFormBlock(fieldSchema) || fieldSchema; return { templateData: _.cloneDeep(formSchema?.['x-data-templates']), }; }; export const SchemaSettingsDataTemplates = function DataTemplates(props) { const designerCtx = useContext(SchemaComponentContext); const { collectionName } = props; const fieldSchema = useFieldSchema(); const { dn } = useDesignable(); const { t } = useTranslation(); const formSchema = findFormBlock(fieldSchema) || fieldSchema; const { templateData } = useDataTemplates(); const schema = useMemo( () => ({ type: 'object', title: t('Form data templates'), properties: { fieldReaction: { 'x-decorator': (props) => , 'x-component': FormDataTemplates, 'x-component-props': { designerCtx, formSchema, useProps: () => { return { defaultValues: templateData, collectionName, }; }, }, }, }, }), [templateData], ); const onSubmit = useCallback((v) => { const data = { ...(formSchema['x-data-templates'] || {}), ...v.fieldReaction }; // 当 Tree 组件开启 checkStrictly 属性时,会导致 checkedKeys 的值是一个对象,而不是数组,所以这里需要转换一下以支持旧版本 data.items.forEach((item) => { item.fields = Array.isArray(item.fields) ? item.fields : item.fields.checked; }); const schema = { ['x-uid']: formSchema['x-uid'], ['x-data-templates']: data, }; formSchema['x-data-templates'] = data; dn.emit('patch', { schema, }); dn.refresh(); }, []); const title = useMemo(() => t('Form data templates'), []); const components = useMemo(() => ({ ArrayCollapse, FormLayout }), []); return ( ); }; export const SchemaSettingsEnableChildCollections = function EnableChildCollectionsItem(props) { const { collectionName } = props; const fieldSchema = useFieldSchema(); const field = useField(); const { dn } = useDesignable(); const { t } = useTranslation(); const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current']; const { getCollectionJoinField } = useCollectionManager(); const ctx = useBlockRequestContext(); const collectionField = getCollectionJoinField(fieldSchema?.parent?.['x-collection-field']) || {}; const isAssocationAdd = fieldSchema?.parent?.['x-component'] === 'CollectionField'; return ( { return { defaultValues: fieldSchema?.['x-enable-children'], collectionName, }; }, }, }, allowAddToCurrent: { type: 'boolean', 'x-content': "{{t('Allow adding records to the current collection')}}", 'x-decorator': 'FormItem', 'x-component': 'Checkbox', default: allowAddToCurrent === undefined ? true : allowAddToCurrent, }, linkageFromForm: { type: 'string', title: "{{t('Linkage with form fields')}}", 'x-visible': '{{isAssocationAdd}}', 'x-decorator': 'FormItem', 'x-component': ChildDynamicComponent, 'x-component-props': { rootCollection: ctx.props.collection || ctx.props.resource, collectionField, }, default: fieldSchema?.['x-component-props']?.['linkageFromForm'], }, }, } 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; fieldSchema['x-allow-add-to-current'] = v.allowAddToCurrent; fieldSchema['x-component-props'] = { ...fieldSchema['x-component-props'], component: 'CreateRecordAction', linkageFromForm: v?.linkageFromForm, }; schema['x-enable-children'] = enableChildren; schema['x-allow-add-to-current'] = v.allowAddToCurrent; schema['x-component-props'] = { ...fieldSchema['x-component-props'], component: 'CreateRecordAction', linkageFromForm: v?.linkageFromForm, }; field.componentProps['linkageFromForm'] = v.linkageFromForm; dn.emit('patch', { schema, }); dn.refresh(); }} /> ); }; export const SchemaSettingsDataFormat = function DateFormatConfig(props: { fieldSchema: Schema }) { const { fieldSchema } = props; const field = useField(); const form = useForm(); const { dn } = useDesignable(); const { t } = useTranslation(); const { getCollectionJoinField } = useCollectionManager(); const collectionField = getCollectionJoinField(fieldSchema?.['x-collection-field']) || {}; const isShowTime = fieldSchema?.['x-component-props']?.showTime; const dateFormatDefaultValue = fieldSchema?.['x-component-props']?.dateFormat || collectionField?.uiSchema?.['x-component-props']?.dateFormat || 'YYYY-MM-DD'; const timeFormatDefaultValue = fieldSchema?.['x-component-props']?.timeFormat || collectionField?.uiSchema?.['x-component-props']?.timeFormat; return ( { field.query('.timeFormat').take(f => { f.display = field.value ? 'visible' : 'none'; }); }}}`, ], }, timeFormat: { type: 'string', title: '{{t("Time format")}}', 'x-component': ExpiresRadio, 'x-decorator': 'FormItem', 'x-decorator-props': { className: css` margin-bottom: 0px; `, }, 'x-component-props': { className: css` color: red; .ant-radio-wrapper { display: flex; margin: 5px 0px; } `, defaultValue: 'h:mm a', formats: ['hh:mm:ss a', 'HH:mm:ss'], timeFormat: true, }, default: timeFormatDefaultValue, enum: [ { label: DateFormatCom({ format: 'hh:mm:ss a' }), value: 'hh:mm:ss a', }, { label: DateFormatCom({ format: 'HH:mm:ss' }), value: 'HH:mm:ss', }, { label: 'custom', value: 'custom', }, ], }, }, } as ISchema } onSubmit={(data) => { const schema = { ['x-uid']: fieldSchema['x-uid'], }; schema['x-component-props'] = fieldSchema['x-component-props'] || {}; fieldSchema['x-component-props'] = { ...(fieldSchema['x-component-props'] || {}), ...data, }; schema['x-component-props'] = fieldSchema['x-component-props']; field.componentProps = fieldSchema['x-component-props']; field.query(`.*.${fieldSchema.name}`).forEach((f) => { f.componentProps = fieldSchema['x-component-props']; }); dn.emit('patch', { schema, }); dn.refresh(); }} /> ); }; const defaultInputStyle = css` & > .nb-form-item { flex: 1; } `; export const findParentFieldSchema = (fieldSchema: Schema) => { let parent = fieldSchema.parent; while (parent) { if (parent['x-component'] === 'CollectionField') { return parent; } parent = parent.parent; } }; export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props: { fieldSchema?: Schema }) { const currentSchema = useFieldSchema(); const fieldSchema = props?.fieldSchema ?? currentSchema; const field: Field = useField(); const { dn } = useDesignable(); const { t } = useTranslation(); const actionCtx = useActionContext(); let targetField; const { getField } = useCollection(); const { getCollectionJoinField, getCollectionFields, getAllCollectionsInheritChain } = useCollectionManager(); const variables = useVariables(); const localVariables = useLocalVariables(); const collection = useCollection(); const record = useRecord(); const { form } = useFormBlockContext(); const { getFields } = useCollectionFilterOptionsV2(collection); const { isInSubForm, isInSubTable } = useFlag() || {}; const { name } = collection; const collectionField = useMemo( () => getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']), [fieldSchema, getCollectionJoinField, getField], ); const fieldSchemaWithoutRequired = _.omit(fieldSchema, 'required'); if (collectionField?.target) { targetField = getCollectionJoinField( `${collectionField.target}.${fieldSchema['x-component-props']?.fieldNames?.label || 'id'}`, ); } const parentFieldSchema = collectionField?.interface === 'm2o' && findParentFieldSchema(fieldSchema); const parentCollectionField = parentFieldSchema && getCollectionJoinField(parentFieldSchema?.['x-collection-field']); const tableCtx = useTableBlockContext(); const isAllowContextVariable = actionCtx?.fieldSchema?.['x-action'] === 'customize:create' && (collectionField?.interface === 'm2m' || (parentCollectionField?.type === 'hasMany' && collectionField?.interface === 'm2o')); const returnScope = useCallback( (scope: Option[]) => { const currentForm = scope.find((item) => item.value === '$nForm'); const fields = getCollectionFields(name); // fix https://nocobase.height.app/T-1355 // 工作流人工节点的 `自定义表单` 区块,与其它表单区块不同,根据它的数据表名称,获取到的字段列表为空,所以需要在这里特殊处理一下 if (!fields?.length && currentForm) { currentForm.children = formatVariableScop(getFields()); } return scope; }, [getFields, name], ); const DefaultValueComponent: any = useMemo(() => { return { ArrayCollapse, FormLayout, VariableInput: (props) => { return ( ); }, }; }, [isInSubForm, isInSubTable]); const schema = useMemo(() => { return { type: 'object', title: t('Set default value'), properties: { default: { 'x-decorator': 'FormItem', 'x-component': 'VariableInput', 'x-component-props': { ...(fieldSchema?.['x-component-props'] || {}), collectionField, contextCollectionName: isAllowContextVariable && tableCtx.collection, schema: collectionField?.uiSchema, targetFieldSchema: fieldSchema, className: defaultInputStyle, form, record, returnScope, shouldChange: getShouldChange({ collectionField, variables, localVariables, getAllCollectionsInheritChain, }), renderSchemaComponent: function Com(props) { const s = _.cloneDeep(fieldSchemaWithoutRequired) || ({} as Schema); s.title = ''; s.name = 'default'; s['x-read-pretty'] = false; s['x-disabled'] = false; const defaultValue = getFieldDefaultValue(s, collectionField); if (collectionField.target && s['x-component-props']) { s['x-component-props'].mode = 'Select'; } if (collectionField?.uiSchema.type) { s.type = collectionField.uiSchema.type; } if (collectionField?.uiSchema['x-component'] === 'Checkbox') { s['x-component-props'].defaultChecked = defaultValue; // 在这里如果不设置 type 为 void,会导致设置的默认值不生效 // 但是我不知道为什么必须要设置为 void ? s.type = 'void'; } const schema = { ...(s || {}), 'x-decorator': 'FormItem', 'x-component-props': { ...s['x-component-props'], collectionName: collectionField?.collectionName, targetField, onChange: props.onChange, defaultValue: isVariable(defaultValue) ? '' : defaultValue, style: { width: '100%', verticalAlign: 'top', minWidth: '200px', }, }, default: isVariable(defaultValue) ? '' : defaultValue, } as ISchema; return ( ); }, }, title: t('Default value'), default: getFieldDefaultValue(fieldSchema, collectionField), }, }, } as ISchema; }, [ collectionField, fieldSchema, fieldSchemaWithoutRequired, form, getAllCollectionsInheritChain, isAllowContextVariable, localVariables, record, returnScope, t, tableCtx.collection, targetField, variables, ]); const handleSubmit: (values: any) => void = useCallback( (v) => { const schema: ISchema = { ['x-uid']: fieldSchema['x-uid'], }; fieldSchema.default = v.default; if (!v.default && v.default !== 0) { field.value = null; } schema.default = v.default; dn.emit('patch', { schema, currentSchema, }); }, [currentSchema, dn, field, fieldSchema], ); return ( ); }; export const SchemaSettingsSortingRule = function SortRuleConfigure(props) { const field = useField(); const { dn } = useDesignable(); const { t } = useTranslation(); const currentSchema = useFieldSchema(); const { getField } = useCollection(); const { getCollectionJoinField } = useCollectionManager(); const fieldSchema = props?.fieldSchema ?? currentSchema; const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); const sortFields = useSortFields(collectionField?.target); const defaultSort = fieldSchema['x-component-props']?.service?.params?.sort || []; const sort = defaultSort?.map((item: string) => { return item?.startsWith('-') ? { field: item.substring(1), direction: 'desc', } : { field: item, direction: 'asc', }; }); return ( { const sortArr = sort.map((item) => { return item.direction === 'desc' ? `-${item.field}` : item.field; }); _.set(field.componentProps, 'service.params.sort', sortArr); props?.onSubmitCallBack?.(sortArr); fieldSchema['x-component-props'] = field.componentProps; dn.emit('patch', { schema: { ['x-uid']: fieldSchema['x-uid'], 'x-component-props': field.componentProps, }, }); }} /> ); }; export const SchemaSettingsDataScope: FC = function DataScopeConfigure(props) { const { t } = useTranslation(); const { getFields } = useCollectionFilterOptionsV2(props.collectionName); const record = useRecord(); const { form } = useFormBlockContext(); const variables = useVariables(); const localVariables = useLocalVariables(); const { getAllCollectionsInheritChain } = useCollectionManager(); const { isInSubForm, isInSubTable } = useFlag() || {}; const dynamicComponent = useCallback( (props: DynamicComponentProps) => { return ( ); }, [form, getAllCollectionsInheritChain, localVariables, record, variables], ); const getSchema = () => { return { type: 'object', title: t('Set the data scope'), properties: { filter: { enum: props.collectionFilterOption || getFields(), 'x-decorator': (props) => ( {props.children} ), 'x-decorator-props': { isDisabled, }, 'x-component': 'Filter', 'x-component-props': { collectionName: props.collectionName, dynamicComponent: props.dynamicComponent || dynamicComponent, }, }, }, }; }; return ( ISchema} onSubmit={props.onSubmit} /> ); }; // 是否是系统字段 export const isSystemField = (collectionField: CollectionFieldOptions, getInterface) => { const i = getInterface?.(collectionField?.interface); return i?.group === 'systemInfo'; }; export const isPatternDisabled = (fieldSchema: Schema) => { return fieldSchema?.['x-component-props']?.['pattern-disable'] == true; }; interface SelectWithTitleProps { title?: any; defaultValue?: any; options?: any; onChange?: (...args: any[]) => void; } export function SelectWithTitle({ title, defaultValue, onChange, options }: SelectWithTitleProps) { const [open, setOpen] = useState(false); const timerRef = useRef(null); return (
{ e.stopPropagation(); setOpen((v) => !v); }} onMouseLeave={() => { timerRef.current = setTimeout(() => { setOpen(false); }, 200); }} > {title}