/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ 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, observer, useField, useFieldSchema, useForm } from '@formily/react'; import { observable } from '@formily/reactive'; import { uid } from '@formily/shared'; import type { DropdownProps } from 'antd'; import { Alert, App, Button, Cascader, CascaderProps, ConfigProvider, Dropdown, Menu, MenuItemProps, MenuProps, Modal, ModalFuncProps, Space, Switch, } from 'antd'; import _, { cloneDeep, get, set } from 'lodash'; import React, { FC, ReactNode, createContext, startTransition, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { SchemaSettingsItemType, SchemaToolbarVisibleContext, VariablesContext, getZIndex, useCollection, useCollectionManager, useZIndexContext, zIndexContext, } from '../'; import { APIClientProvider } from '../api-client/APIClientProvider'; import { useAPIClient } from '../api-client/hooks/useAPIClient'; import { ApplicationContext, LocationSearchContext, useApp, useLocationSearch } from '../application'; import { BlockContext, BlockRequestContext_deprecated, useBlockContext, useBlockRequestContext, } from '../block-provider/BlockProvider'; import { CollectOperators, useOperators } from '../block-provider/CollectOperators'; import { FormBlockContext, findFormBlock, useFormBlockContext, useFormBlockType, } from '../block-provider/FormBlockProvider'; import { FormActiveFieldsProvider, useFormActiveFields } from '../block-provider/hooks'; import { useLinkageCollectionFilterOptions, useSortFields } from '../collection-manager/action-hooks'; import { useCollectionManager_deprecated } from '../collection-manager/hooks/useCollectionManager_deprecated'; import { CollectionFieldOptions_deprecated } from '../collection-manager/types'; import { SelectWithTitle, SelectWithTitleProps } from '../common/SelectWithTitle'; import { useNiceDropdownMaxHeight } from '../common/useNiceDropdownHeight'; import { CollectionRecordProvider, useCollectionRecord, } from '../data-source/collection-record/CollectionRecordProvider'; import { DataSourceApplicationProvider } from '../data-source/components/DataSourceApplicationProvider'; import { AssociationOrCollectionProvider, useDataBlockProps } from '../data-source/data-block/DataBlockProvider'; import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider'; import { useDataSourceKey } from '../data-source/data-source/DataSourceProvider'; import { useFilterBlock } from '../filter-provider/FilterProvider'; import { FlagProvider, useFlag } from '../flag-provider'; import { useGlobalTheme } from '../global-theme'; import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem'; import { VariablePopupRecordProvider, useCurrentPopupRecord, useParentPopupRecord, } from '../modules/variable/variablesProvider/VariablePopupRecordProvider'; import { useRecord } from '../record-provider'; import { ActionContextProvider } from '../schema-component/antd/action/context'; import { SubFormProvider, useSubFormValue } from '../schema-component/antd/association-field/hooks'; import { FormDialog } from '../schema-component/antd/form-dialog'; import { AllDataBlocksContext } from '../schema-component/antd/page/AllDataBlocksProvider'; import { SchemaComponentContext } from '../schema-component/context'; import { FormProvider } from '../schema-component/core/FormProvider'; import { RemoteSchemaComponent } from '../schema-component/core/RemoteSchemaComponent'; import { SchemaComponent } from '../schema-component/core/SchemaComponent'; import { SchemaComponentOptions } from '../schema-component/core/SchemaComponentOptions'; import { useCompile } from '../schema-component/hooks/useCompile'; import { Designable, createDesignable, useDesignable } from '../schema-component/hooks/useDesignable'; import { useSchemaTemplateManager } from '../schema-templates'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplateProvider'; import { useLocalVariables, useVariables } from '../variables'; import { FormDataTemplates } from './DataTemplates'; import { EnableChildCollections } from './EnableChildCollections'; import { ChildDynamicComponent } from './EnableChildCollections/DynamicComponent'; import { FormLinkageRules } from './LinkageRules'; import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks'; import { LinkageRuleCategory, LinkageRuleDataKeyMap } from './LinkageRules/type'; import { CurrentRecordContextProvider, useCurrentRecord } from './VariableInput/hooks/useRecordVariable'; export interface SchemaSettingsProps { title?: any; dn?: Designable; field?: GeneralField; fieldSchema?: Schema; children?: ReactNode; mode?: 'inline' | 'dropdown'; } interface SchemaSettingsContextProps { dn?: Designable; field?: GeneralField; fieldSchema?: Schema; setVisible?: any; visible?: any; template?: any; collectionName?: any; designer?: T; } const SchemaSettingsContext = createContext(null); SchemaSettingsContext.displayName = 'SchemaSettingsContext'; 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 } = props; const { getTemplateBySchema } = useSchemaTemplateManager(); const collection = useCollection(); const template = getTemplateBySchema(fieldSchema); const value = useMemo( () => ({ ...props, collectionName: collection?.name, template, fieldSchema, }), [collection?.name, fieldSchema, props, template], ); return {children}; }; const InternalSchemaSettingsDropdown: React.FC = React.memo((props) => { const { title, dn, ...others } = props; const [visible, setVisible] = useState(false); const { Component, getMenuItems } = useMenuItem(); const dropdownMaxHeight = useNiceDropdownMaxHeight([visible]); // 单测中需要在首次就把菜单渲染出来,否则不会触发菜单的渲染进而报错。原因未知。 const [openDropdown, setOpenDropdown] = useState(process.env.__TEST__ ? true : false); const toolbarVisible = useContext(SchemaToolbarVisibleContext); useEffect(() => { if (toolbarVisible) { setOpenDropdown(false); } }, [toolbarVisible]); const changeMenu: DropdownProps['onOpenChange'] = (nextOpen: boolean, info) => { if (info.source === 'trigger' || nextOpen) { // 当鼠标快速滑过时,终止菜单的渲染,防止卡顿 startTransition(() => { setVisible(nextOpen); }); } }; const handleMouseEnter = () => { setOpenDropdown(true); }; // 从这里截断,可以保证每次显示时都是最新状态的菜单列表 if (!openDropdown) { return (
{typeof title === 'string' ? {title} : title}
); } const items = getMenuItems(() => props.children); return (
{typeof title === 'string' ? {title} : title}
); }); const InternalSchemaSettingsMenu: React.FC = React.memo((props) => { const { title, dn, ...others } = props; const [visible, setVisible] = useState(true); const { Component, getMenuItems } = useMenuItem(); const items = getMenuItems(() => props.children); return ( ); }); export const SchemaSettingsDropdown: React.FC = React.memo((props) => { const { mode } = props; return mode === 'inline' ? : ; }); SchemaSettingsDropdown.displayName = 'SchemaSettingsDropdown'; const findGridSchema = (fieldSchema) => { return fieldSchema.reduceProperties((buf, s) => { if (s['x-component'] === 'FormV2' || s['x-component'] === 'Details') { 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' || s['x-component'] === 'Details') { 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 cm = 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, 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 collection = collectionName && cm?.getCollection(collectionName); const gridSchema = findGridSchema(fieldSchema); const values = await FormDialog( t('Save as reference template'), () => { const componentTitle = { FormItem: t('Form'), ReadPrettyFormItem: t('Details'), }; return ( ); }, theme, ).open({}); const sdn = createDesignable({ t, api, current: gridSchema.parent, }); sdn.loadAPIClientEvents(); const { key } = await saveAsTemplate({ collectionName, resourceName, componentName, dataSourceKey: collection.dataSource, 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, }, }); dn.refresh(); }} > {t('Save as reference template')} ); }; export interface SchemaSettingsItemProps extends Omit { title: string | ReactNode; } export const SchemaSettingsItem: FC = (props) => { const { pushMenuItem } = useCollectMenuItems(); const { collectMenuItem } = useCollectMenuItem(); const { eventKey, title } = props; 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 { disabled?: boolean; title?: string; confirm?: ModalFuncProps; removeParentsIfNoChildren?: boolean; breakRemoveOn?: ISchema | ((s: ISchema) => boolean); } export const SchemaSettingsRemove: FC = (props) => { const { disabled, confirm, title, removeParentsIfNoChildren, breakRemoveOn } = props; const { dn, template } = useSchemaSettings(); const { t } = useTranslation(); const field = useField(); const compile = useCompile(); const fieldSchema = useFieldSchema(); const ctx = useBlockTemplateContext(); const form = useForm(); const { modal } = App.useApp(); const { removeActiveFieldName } = useFormActiveFields() || {}; const { removeDataBlock } = useFilterBlock(); return ( { modal.confirm({ title: title ? compile(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 && template.uid === fieldSchema['x-uid'] && ctx?.dn) { await ctx?.dn.remove(null, options); } else { await dn.remove(null, options); } await confirm?.onOk?.(); delete form.values[fieldSchema.name]; dn.refresh({ refreshParentSchema: true }); removeActiveFieldName?.(fieldSchema.name as string); form?.query(new RegExp(`${fieldSchema.parent.name}.${fieldSchema.name}$`)).forEach((field: Field) => { // 如果字段被删掉,那么在提交的时候不应该提交这个字段 field.setValue?.(undefined); field.setInitialValue?.(undefined); }); removeDataBlock(fieldSchema['x-uid']); }, }); }} > {t('Delete')} ); }; export interface SchemaSettingsSelectItemProps extends Omit, Omit { value?: SelectWithTitleProps['defaultValue']; optionRender?: (option: any, info: { index: number }) => React.ReactNode; } export const SchemaSettingsSelectItem: FC = (props) => { const { title, options, value, onChange, optionRender, ...others } = props; return ( ); }; export type SchemaSettingsCascaderItemProps = CascaderProps & Omit & { title: any }; export const SchemaSettingsCascaderItem: FC = (props) => { const { title, options, value, onChange, ...others } = props; return (
{title}
); }; const ml32 = { marginLeft: 32 }; const MenuItemSwitch: FC<{ state: { checked: boolean } }> = observer(({ state }) => { return ; }); MenuItemSwitch.displayName = 'MenuItemSwitch'; export interface SchemaSettingsSwitchItemProps extends Omit { title: string | ReactNode; checked?: boolean; onChange?: (v: boolean) => void; } export const SchemaSettingsSwitchItem: FC = (props) => { const { title, onChange, ...others } = props; const [state] = useState(() => observable({ checked: !!props.checked })); return ( { onChange?.(!state.checked); state.checked = !state.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 { uid?: string; initialSchema?: ISchema; schema?: ISchema; beforeOpen?: () => void; maskClosable?: boolean; width?: string | number; } export const SchemaSettingsActionModalItem: FC = React.memo((props) => { const { title, onSubmit, width = '50%', 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 parentZIndex = useZIndexContext(); const zIndex = getZIndex('modal', parentZIndex + 10, 0); 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(); try { const allValues = form.values; // 过滤掉那些在表单 Schema 中未定义的字段 const visibleValues = Object.keys(allValues).reduce((result, key) => { if (form.query(key).take()) { result[key] = allValues[key]; } return result; }, {}); setVisible(false); await onSubmit?.(cloneDeep(visibleValues)); } catch (err) { console.error(err); } }, [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) => Promise | 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; /** 上下文中不需要当前记录 */ noRecord?: boolean; /** 自定义 Modal 上下文 */ ModalContextProvider?: React.FC; dialogRootClassName?: string; } export const SchemaSettingsModalItem: FC = (props) => { const { hidden, title, components, scope, effects, onSubmit, asyncGetInitialValues, initialValues, width = 'fit-content', noRecord = false, ModalContextProvider = (props) => <>{props.children}, dialogRootClassName, ...others } = props; const options = useContext(SchemaOptionsContext); const collection = useCollection(); const apiClient = useAPIClient(); const app = useApp(); const { theme } = useGlobalTheme(); const ctx = useBlockRequestContext(); const upLevelActiveFields = useFormActiveFields(); const { locale } = useContext(ConfigProvider.ConfigContext); const dm = useDataSourceManager(); const dataSourceKey = useDataSourceKey(); const record = useCollectionRecord(); const { association } = useDataBlockProps() || {}; const currentRecordCtx = useCurrentRecord(); const formCtx = useFormBlockContext(); const blockOptions = useBlockContext(); const { getOperators } = useOperators(); const locationSearch = useLocationSearch(); const variableOptions = useVariables(); const allDataBlocks = useContext(AllDataBlocksContext); // 解决变量`当前对象`值在弹窗中丢失的问题 const { formValue: subFormValue, collection: subFormCollection, parent } = useSubFormValue(); // 解决弹窗变量丢失的问题 const popupRecordVariable = useCurrentPopupRecord(); const parentPopupRecordVariable = useParentPopupRecord(); 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, rootClassName: dialogRootClassName }, () => { 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 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 collection = 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 || collection?.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, readPretty, Component, afterSubmit } = 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 category = props?.category ?? LinkageRuleCategory.default; const elementType = props?.type || (fieldSchema?.['x-action'] || ['Action', 'Action.Link'].includes(fieldSchema['x-component']) ? 'button' : 'field'); const gridSchema = findGridSchema(fieldSchema) || fieldSchema; const options = useLinkageCollectionFilterOptions(collectionName); const linkageOptions = useLinkageCollectionFieldOptions(collectionName, readPretty); const titleMap = { [LinkageRuleCategory.default]: t('Linkage rules'), [LinkageRuleCategory.style]: t('Style'), }; const dataKey = LinkageRuleDataKeyMap[category]; const getRules = useCallback(() => { return gridSchema?.[dataKey] || fieldSchema?.[dataKey] || []; }, [gridSchema, fieldSchema, dataKey]); const title = titleMap[category] || t('Linkage rules'); const flagVales = useFlag(); const schema = useMemo( () => ({ type: 'object', title, properties: { fieldReaction: { 'x-component': Component || FormLinkageRules, 'x-use-component-props': () => { return { options, defaultValues: getRules(), linkageOptions, category, elementType, collectionName, form, variables, localVariables, record, formBlockType, }; }, }, }, }), [collectionName, fieldSchema, form, gridSchema, localVariables, record, t, variables, getRules, Component], ); const components = useMemo(() => ({ ArrayCollapse, FormLayout }), []); const onSubmit = useCallback( (v) => { const rules = []; for (const rule of v.fieldReaction.rules) { rules.push(_.omit(_.pickBy(rule, _.identity), ['conditionBasic', 'conditionAdvanced'])); } 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[dataKey] = rules; schema[dataKey] = rules; dn.emit('patch', { schema, }); dn.refresh(); afterSubmit?.(); }, [dn, getTemplateById, gridSchema, dataKey, afterSubmit], ); return ( { return {props.children}; }} /> ); }; 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-use-component-props': () => { return { defaultValues: templateData, collectionName, }; }, 'x-component-props': { designerCtx, formSchema, }, }, }, }), [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 function SchemaSettingsEnableChildCollections(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_deprecated(); 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 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 schemaSettingsLabelLayout: SchemaSettingsItemType = { name: 'formLabelLayout', type: 'select', useComponentProps() { const field = useField(); const fieldSchema = useFieldSchema(); const { t } = useTranslation(); const { dn } = useDesignable(); return { title: t('Layout'), value: field.componentProps?.layout || 'vertical', options: [ { label: t('Vertical'), value: 'vertical' }, { label: t('Horizontal'), value: 'horizontal' }, ], onChange: (layout) => { field.componentProps = field.componentProps || {}; field.componentProps.layout = layout; fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; fieldSchema['x-component-props']['layout'] = layout; dn.emit('patch', { schema: { ['x-uid']: fieldSchema['x-uid'], 'x-component-props': fieldSchema['x-component-props'], }, }); }, }; }, }; // 是否是系统字段 export const isSystemField = (collectionField: CollectionFieldOptions_deprecated, getInterface) => { const i = getInterface?.(collectionField?.interface); return i?.group === 'systemInfo'; }; export function getFieldDefaultValue(fieldSchema: ISchema, collectionField: CollectionFieldOptions_deprecated) { const result = fieldSchema?.default ?? collectionField?.defaultValue; return result; }