feat: add support for opening via URL (#5098)
Some checks are pending
auto-merge / push-commit (push) Waiting to run
Build Docker Image / build-and-push (push) Waiting to run
Build Pro Image / app-token (push) Waiting to run
Build Pro Image / build-and-push (push) Blocked by required conditions
deploy client docs / Build (push) Waiting to run
E2E / Build (push) Waiting to run
E2E / Core and plugins (push) Blocked by required conditions
E2E / plugin-workflow (push) Blocked by required conditions
E2E / plugin-workflow-approval (push) Blocked by required conditions
E2E / plugin-data-source-main (push) Blocked by required conditions
E2E / Comment on PR (push) Blocked by required conditions
NocoBase Backend Test / sqlite-test (20, false) (push) Waiting to run
NocoBase Backend Test / sqlite-test (20, true) (push) Waiting to run
NocoBase Backend Test / postgres-test (public, 20, nocobase, false) (push) Waiting to run
NocoBase Backend Test / postgres-test (public, 20, nocobase, true) (push) Waiting to run
NocoBase Backend Test / postgres-test (public, 20, public, false) (push) Waiting to run
NocoBase Backend Test / postgres-test (public, 20, public, true) (push) Waiting to run
NocoBase Backend Test / postgres-test (user_schema, 20, nocobase, false) (push) Waiting to run
NocoBase Backend Test / postgres-test (user_schema, 20, nocobase, true) (push) Waiting to run
NocoBase Backend Test / postgres-test (user_schema, 20, public, false) (push) Waiting to run
NocoBase Backend Test / postgres-test (user_schema, 20, public, true) (push) Waiting to run
NocoBase Backend Test / mysql-test (20, false) (push) Waiting to run
NocoBase Backend Test / mysql-test (20, true) (push) Waiting to run
NocoBase Backend Test / mariadb-test (20, false) (push) Waiting to run
NocoBase Backend Test / mariadb-test (20, true) (push) Waiting to run
NocoBase FrontEnd Test / frontend-test (18) (push) Waiting to run
Test on Windows / build (push) Waiting to run

* feat(map): add support for opening via URL

* feat(calendar): add support for opening via URL

* feat(gantt): add support for opening via URL

* fix(duplicate,bulk-edit): resolve issues with popups

* fix: useDetailsBlockProps

---------

Co-authored-by: katherinehhh <katherine_15995@163.com>
This commit is contained in:
Zeke Zhang 2024-08-27 21:58:55 +08:00 committed by GitHub
parent ce3d6ac233
commit dec3c838a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 419 additions and 257 deletions

View File

@ -141,6 +141,7 @@ export const useDetailsBlockProps = () => {
ctx.form ctx.form
.reset() .reset()
.then(() => { .then(() => {
ctx.form.setInitialValues(data || {});
ctx.form.setValues(data || {}); ctx.form.setValues(data || {});
}) })
.catch(console.error); .catch(console.error);

View File

@ -17,7 +17,10 @@ import { PopupVisibleProvider, PopupVisibleProviderContext } from '../../schema-
* @param props * @param props
* @returns * @returns
*/ */
export const PopupContextProvider: React.FC = (props) => { export const PopupContextProvider: React.FC<{
visible?: boolean;
setVisible?: (visible: boolean) => void;
}> = (props) => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { visible: visibleWithURL, setVisible: setVisibleWithURL } = useContext(PopupVisibleProviderContext) || { const { visible: visibleWithURL, setVisible: setVisibleWithURL } = useContext(PopupVisibleProviderContext) || {
visible: false, visible: false,
@ -26,10 +29,11 @@ export const PopupContextProvider: React.FC = (props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const _setVisible = useCallback( const _setVisible = useCallback(
(value: boolean): void => { (value: boolean): void => {
props.setVisible?.(value);
setVisible?.(value); setVisible?.(value);
setVisibleWithURL?.(value); setVisibleWithURL?.(value);
}, },
[setVisibleWithURL], [props, setVisibleWithURL],
); );
const openMode = fieldSchema['x-component-props']?.['openMode'] || 'drawer'; const openMode = fieldSchema['x-component-props']?.['openMode'] || 'drawer';
const openSize = fieldSchema['x-component-props']?.['openSize']; const openSize = fieldSchema['x-component-props']?.['openSize'];
@ -37,7 +41,7 @@ export const PopupContextProvider: React.FC = (props) => {
return ( return (
<PopupVisibleProvider visible={false}> <PopupVisibleProvider visible={false}>
<ActionContextProvider <ActionContextProvider
visible={visible || visibleWithURL} visible={props.visible || visible || visibleWithURL}
setVisible={_setVisible} setVisible={_setVisible}
openMode={openMode} openMode={openMode}
openSize={openSize} openSize={openSize}

View File

@ -33,7 +33,7 @@ interface PopupsVisibleProviderProps {
setVisible?: (value: boolean) => void; setVisible?: (value: boolean) => void;
} }
interface PopupProps { export interface PopupProps {
params: PopupParams; params: PopupParams;
context: PopupContext; context: PopupContext;
/** /**

View File

@ -7,13 +7,31 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { useCallback } from 'react'; import React, { FC, useCallback, useMemo } from 'react';
const PopupSettingsContext = React.createContext({
enableURL: true,
});
export const PopupSettingsProvider: FC<{
/**
* @default true
*/
enableURL?: boolean;
}> = (props) => {
const { enableURL = true } = props;
const value = useMemo(() => ({ enableURL }), [enableURL]);
return <PopupSettingsContext.Provider value={value}>{props.children}</PopupSettingsContext.Provider>;
};
/** /**
* Hook for accessing the popup settings. * Hook for accessing the popup settings.
* @returns The popup settings. * @returns The popup settings.
*/ */
export const usePopupSettings = () => { export const usePopupSettings = () => {
const { enableURL } = React.useContext(PopupSettingsContext);
const isPopupVisibleControlledByURL = useCallback(() => { const isPopupVisibleControlledByURL = useCallback(() => {
const pathname = window.location.pathname; const pathname = window.location.pathname;
const hash = window.location.hash; const hash = window.location.hash;
@ -21,8 +39,8 @@ export const usePopupSettings = () => {
const isNewMobileMode = pathname?.includes('/m/'); const isNewMobileMode = pathname?.includes('/m/');
const isPCMode = pathname?.includes('/admin/'); const isPCMode = pathname?.includes('/admin/');
return (isPCMode || isNewMobileMode) && !isOldMobileMode; return (isPCMode || isNewMobileMode) && !isOldMobileMode && enableURL;
}, []); }, [enableURL]);
return { return {
/** 弹窗窗口的显隐是否由 URL 控制 */ /** 弹窗窗口的显隐是否由 URL 控制 */

View File

@ -15,3 +15,4 @@ export * from './Page.Settings';
export { PagePopups } from './PagePopups'; export { PagePopups } from './PagePopups';
export { storePopupContext } from './pagePopupUtils'; export { storePopupContext } from './pagePopupUtils';
export * from './PageTab.Settings'; export * from './PageTab.Settings';
export { PopupSettingsProvider } from './PopupSettingsProvider';

View File

@ -127,7 +127,16 @@ export const getPopupPathFromParams = (params: PopupParams) => {
* Note: use this hook in a plugin is not recommended * Note: use this hook in a plugin is not recommended
* @returns * @returns
*/ */
export const usePopupUtils = () => { export const usePopupUtils = (
options: {
/**
* when the popup does not support opening via URL, you can control the display status of the popup through this method
* @param visible
* @returns
*/
setVisible?: (visible: boolean) => void;
} = {},
) => {
const navigate = useNavigateNoUpdate(); const navigate = useNavigateNoUpdate();
const location = useLocationNoUpdate(); const location = useLocationNoUpdate();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
@ -141,14 +150,16 @@ export const usePopupUtils = () => {
const { params: popupParams } = useCurrentPopupContext(); const { params: popupParams } = useCurrentPopupContext();
const service = useDataBlockRequest(); const service = useDataBlockRequest();
const { isPopupVisibleControlledByURL } = usePopupSettings(); const { isPopupVisibleControlledByURL } = usePopupSettings();
const { setVisible: setVisibleFromAction } = useContext(ActionContext); const { setVisible: _setVisibleFromAction } = useContext(ActionContext);
const { updatePopupContext } = usePopupContextInActionOrAssociationField(); const { updatePopupContext } = usePopupContextInActionOrAssociationField();
const currentPopupContext = useCurrentPopupContext();
const getSourceId = useCallback( const getSourceId = useCallback(
(_parentRecordData?: Record<string, any>) => (_parentRecordData?: Record<string, any>) =>
(_parentRecordData || parentRecord?.data)?.[cm.getSourceKeyByAssociation(association)], (_parentRecordData || parentRecord?.data)?.[cm.getSourceKeyByAssociation(association)],
[parentRecord, association], [parentRecord, association],
); );
const currentPopupUidWithoutOpened = fieldSchema?.['x-uid'];
const setVisibleFromAction = options.setVisible || _setVisibleFromAction;
const getNewPathname = useCallback( const getNewPathname = useCallback(
({ ({
@ -199,6 +210,7 @@ export const usePopupUtils = () => {
parentRecordData, parentRecordData,
collectionNameUsedInURL, collectionNameUsedInURL,
popupUidUsedInURL, popupUidUsedInURL,
customActionSchema,
}: { }: {
recordData?: Record<string, any>; recordData?: Record<string, any>;
parentRecordData?: Record<string, any>; parentRecordData?: Record<string, any>;
@ -206,11 +218,13 @@ export const usePopupUtils = () => {
collectionNameUsedInURL?: string; collectionNameUsedInURL?: string;
/** if this value exists, it will be saved in the URL */ /** if this value exists, it will be saved in the URL */
popupUidUsedInURL?: string; popupUidUsedInURL?: string;
customActionSchema?: ISchema;
} = {}) => { } = {}) => {
if (!isPopupVisibleControlledByURL()) { if (!isPopupVisibleControlledByURL()) {
return setVisibleFromAction?.(true); return setVisibleFromAction?.(true);
} }
const currentPopupUidWithoutOpened = customActionSchema?.['x-uid'] || fieldSchema?.['x-uid'];
const sourceId = getSourceId(parentRecordData); const sourceId = getSourceId(parentRecordData);
recordData = recordData || record?.data; recordData = recordData || record?.data;
@ -227,7 +241,7 @@ export const usePopupUtils = () => {
} }
storePopupContext(currentPopupUidWithoutOpened, { storePopupContext(currentPopupUidWithoutOpened, {
schema: fieldSchema, schema: customActionSchema || fieldSchema,
record: new CollectionRecord({ isNew: false, data: recordData }), record: new CollectionRecord({ isNew: false, data: recordData }),
parentRecord: parentRecordData ? new CollectionRecord({ isNew: false, data: parentRecordData }) : parentRecord, parentRecord: parentRecordData ? new CollectionRecord({ isNew: false, data: parentRecordData }) : parentRecord,
service, service,
@ -237,7 +251,7 @@ export const usePopupUtils = () => {
sourceId, sourceId,
}); });
updatePopupContext(getPopupContext()); updatePopupContext(getPopupContext(), customActionSchema);
navigate(withSearchParams(`${url}${pathname}`)); navigate(withSearchParams(`${url}${pathname}`));
}, },
@ -256,7 +270,6 @@ export const usePopupUtils = () => {
isPopupVisibleControlledByURL, isPopupVisibleControlledByURL,
getSourceId, getSourceId,
getPopupContext, getPopupContext,
currentPopupUidWithoutOpened,
], ],
); );
@ -317,6 +330,7 @@ export const usePopupUtils = () => {
closePopup, closePopup,
savePopupSchemaToSchema, savePopupSchemaToSchema,
getPopupSchemaFromSchema, getPopupSchemaFromSchema,
context: currentPopupContext,
/** /**
* @deprecated * @deprecated
* TODO: remove this * TODO: remove this

View File

@ -29,18 +29,19 @@ export const usePopupContextInActionOrAssociationField = () => {
const { dn } = useDesignable(); const { dn } = useDesignable();
const updatePopupContext = useCallback( const updatePopupContext = useCallback(
(context: PopupContext) => { (context: PopupContext, customSchema?: ISchema) => {
customSchema = customSchema || fieldSchema;
context = _.omitBy(context, _.isNil) as PopupContext; context = _.omitBy(context, _.isNil) as PopupContext;
if (_.isEqual(context, getPopupContextFromActionOrAssociationFieldSchema(fieldSchema))) { if (_.isEqual(context, getPopupContextFromActionOrAssociationFieldSchema(customSchema))) {
return; return;
} }
fieldSchema[CONTEXT_SCHEMA_KEY] = context; customSchema[CONTEXT_SCHEMA_KEY] = context;
return dn.emit('patch', { return dn.emit('patch', {
schema: { schema: {
'x-uid': fieldSchema['x-uid'], 'x-uid': customSchema['x-uid'],
[CONTEXT_SCHEMA_KEY]: context, [CONTEXT_SCHEMA_KEY]: context,
}, },
}); });

View File

@ -0,0 +1,19 @@
/**
* 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 { ACLActionProvider, PopupSettingsProvider } from '@nocobase/client';
import React, { FC } from 'react';
export const BulkEditActionDecorator: FC = (props) => {
return (
<PopupSettingsProvider enableURL={false}>
<ACLActionProvider>{props.children}</ACLActionProvider>
</PopupSettingsProvider>
);
};

View File

@ -9,6 +9,7 @@
import { Plugin, useActionAvailable } from '@nocobase/client'; import { Plugin, useActionAvailable } from '@nocobase/client';
import { bulkEditActionSettings, deprecatedBulkEditActionSettings } from './BulkEditAction.Settings'; import { bulkEditActionSettings, deprecatedBulkEditActionSettings } from './BulkEditAction.Settings';
import { BulkEditActionDecorator } from './BulkEditActionDecorator';
import { BulkEditActionInitializer } from './BulkEditActionInitializer'; import { BulkEditActionInitializer } from './BulkEditActionInitializer';
import { import {
BulkEditBlockInitializers_deprecated, BulkEditBlockInitializers_deprecated,
@ -25,7 +26,7 @@ import { BulkEditField } from './component/BulkEditField';
import { useCustomizeBulkEditActionProps } from './utils'; import { useCustomizeBulkEditActionProps } from './utils';
export class PluginActionBulkEditClient extends Plugin { export class PluginActionBulkEditClient extends Plugin {
async load() { async load() {
this.app.addComponents({ BulkEditField }); this.app.addComponents({ BulkEditField, BulkEditActionDecorator });
this.app.addScopes({ useCustomizeBulkEditActionProps }); this.app.addScopes({ useCustomizeBulkEditActionProps });
this.app.schemaSettingsManager.add(deprecatedBulkEditActionSettings); this.app.schemaSettingsManager.add(deprecatedBulkEditActionSettings);
this.app.schemaSettingsManager.add(bulkEditActionSettings); this.app.schemaSettingsManager.add(bulkEditActionSettings);
@ -45,7 +46,7 @@ export class PluginActionBulkEditClient extends Plugin {
Component: BulkEditActionInitializer, Component: BulkEditActionInitializer,
schema: { schema: {
'x-align': 'right', 'x-align': 'right',
'x-decorator': 'ACLActionProvider', 'x-decorator': 'BulkEditActionDecorator',
'x-action': 'customize:bulkEdit', 'x-action': 'customize:bulkEdit',
'x-toolbar': 'ActionSchemaToolbar', 'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:bulkEdit', 'x-settings': 'actionSettings:bulkEdit',

View File

@ -13,6 +13,7 @@ import {
ActionContextProvider, ActionContextProvider,
CollectionProvider_deprecated, CollectionProvider_deprecated,
FormBlockContext, FormBlockContext,
PopupSettingsProvider,
RecordProvider, RecordProvider,
fetchTemplateData, fetchTemplateData,
useACLActionParamsContext, useACLActionParamsContext,
@ -203,7 +204,9 @@ export const DuplicateAction = observer(
{/* 这里的 record 就是弹窗中创建表单的 sourceRecord */} {/* 这里的 record 就是弹窗中创建表单的 sourceRecord */}
<RecordProvider record={{ ...parentRecordData, __collection: duplicateCollection || __collection }}> <RecordProvider record={{ ...parentRecordData, __collection: duplicateCollection || __collection }}>
<ActionContextProvider value={{ ...ctx, visible, setVisible }}> <ActionContextProvider value={{ ...ctx, visible, setVisible }}>
<PopupSettingsProvider enableURL={false}>
<RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties /> <RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties />
</PopupSettingsProvider>
</ActionContextProvider> </ActionContextProvider>
</RecordProvider> </RecordProvider>
</CollectionProvider_deprecated> </CollectionProvider_deprecated>

View File

@ -0,0 +1,15 @@
/**
* 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 { ACLActionProvider } from '@nocobase/client';
import React, { FC } from 'react';
export const DuplicateActionDecorator: FC = (props) => {
return <ACLActionProvider>{props.children}</ACLActionProvider>;
};

View File

@ -19,7 +19,7 @@ export const DuplicateActionInitializer = (props) => {
'x-acl-action': 'create', 'x-acl-action': 'create',
title: '{{ t("Duplicate") }}', title: '{{ t("Duplicate") }}',
'x-component': 'Action.Link', 'x-component': 'Action.Link',
'x-decorator': 'ACLActionProvider', 'x-decorator': 'DuplicateActionDecorator',
'x-component-props': { 'x-component-props': {
openMode: defaultOpenMode, openMode: defaultOpenMode,
component: 'DuplicateAction', component: 'DuplicateAction',

View File

@ -10,6 +10,7 @@
import { Plugin, useActionAvailable } from '@nocobase/client'; import { Plugin, useActionAvailable } from '@nocobase/client';
import { DuplicateAction } from './DuplicateAction'; import { DuplicateAction } from './DuplicateAction';
import { deprecatedDuplicateActionSettings, duplicateActionSettings } from './DuplicateAction.Settings'; import { deprecatedDuplicateActionSettings, duplicateActionSettings } from './DuplicateAction.Settings';
import { DuplicateActionDecorator } from './DuplicateActionDecorator';
import { DuplicateActionInitializer } from './DuplicateActionInitializer'; import { DuplicateActionInitializer } from './DuplicateActionInitializer';
import { DuplicatePluginProvider } from './DuplicatePluginProvider'; import { DuplicatePluginProvider } from './DuplicatePluginProvider';
@ -19,6 +20,7 @@ export class PluginActionDuplicateClient extends Plugin {
this.app.addComponents({ this.app.addComponents({
DuplicateActionInitializer, DuplicateActionInitializer,
DuplicateAction, DuplicateAction,
DuplicateActionDecorator,
}); });
this.app.schemaSettingsManager.add(deprecatedDuplicateActionSettings); this.app.schemaSettingsManager.add(deprecatedDuplicateActionSettings);
this.app.schemaSettingsManager.add(duplicateActionSettings); this.app.schemaSettingsManager.add(duplicateActionSettings);
@ -31,7 +33,7 @@ export class PluginActionDuplicateClient extends Plugin {
'x-action': 'duplicate', 'x-action': 'duplicate',
'x-toolbar': 'ActionSchemaToolbar', 'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:duplicate', 'x-settings': 'actionSettings:duplicate',
'x-decorator': 'ACLActionProvider', 'x-decorator': 'DuplicateActionDecorator',
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'primary',
}, },

View File

@ -10,12 +10,12 @@
import { LeftOutlined, RightOutlined } from '@ant-design/icons'; import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { RecursionField, Schema, observer, useFieldSchema } from '@formily/react'; import { RecursionField, Schema, observer, useFieldSchema } from '@formily/react';
import { import {
ActionContextProvider, PopupContextProvider,
RecordProvider, RecordProvider,
VariablePopupRecordProvider,
getLabelFormatValue, getLabelFormatValue,
useCollection, useCollection,
useCollectionParentRecordData, useCollectionParentRecordData,
usePopupUtils,
useProps, useProps,
withDynamicSchemaProps, withDynamicSchemaProps,
} from '@nocobase/client'; } from '@nocobase/client';
@ -23,10 +23,11 @@ import { parseExpression } from 'cron-parser';
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import get from 'lodash/get'; import get from 'lodash/get';
import React, { useCallback, useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Calendar as BigCalendar, View, dayjsLocalizer } from 'react-big-calendar'; import { Calendar as BigCalendar, View, dayjsLocalizer } from 'react-big-calendar';
import * as dates from 'react-big-calendar/lib/utils/dates'; import * as dates from 'react-big-calendar/lib/utils/dates';
import { i18nt, useTranslation } from '../../locale'; import { i18nt, useTranslation } from '../../locale';
import { CalendarRecordViewer, findEventSchema } from './CalendarRecordViewer';
import Header from './components/Header'; import Header from './components/Header';
import { CalendarToolbarContext } from './context'; import { CalendarToolbarContext } from './context';
import GlobalStyle from './global.style'; import GlobalStyle from './global.style';
@ -160,54 +161,24 @@ const useEvents = (dataSource: any, fieldNames: any, date: Date, view: (typeof W
}, [dataSource, fieldNames.start, fieldNames.end, fieldNames.id, fieldNames.title, date, view, t]); }, [dataSource, fieldNames.start, fieldNames.end, fieldNames.id, fieldNames.title, date, view, t]);
}; };
const CalendarRecordViewer = (props) => {
const { visible, setVisible, record } = props;
const { t } = useTranslation();
const collection = useCollection();
const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema();
const eventSchema: Schema = useMemo(
() =>
fieldSchema.reduceProperties((buf, current) => {
if (current['x-component'].endsWith('.Event')) {
return current;
}
return buf;
}, null),
[],
);
const close = useCallback(() => {
setVisible(false);
}, []);
return (
eventSchema && (
<DeleteEventContext.Provider value={{ close }}>
<ActionContextProvider value={{ visible, setVisible }}>
<RecordProvider record={record} parent={parentRecordData}>
<VariablePopupRecordProvider recordData={record} collection={collection}>
<RecursionField schema={eventSchema} name={eventSchema.name} />
</VariablePopupRecordProvider>
</RecordProvider>
</ActionContextProvider>
</DeleteEventContext.Provider>
)
);
};
export const Calendar: any = withDynamicSchemaProps( export const Calendar: any = withDynamicSchemaProps(
observer( observer(
(props: any) => { (props: any) => {
const [visible, setVisible] = useState(false);
const { openPopup } = usePopupUtils({
setVisible,
});
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { dataSource, fieldNames, showLunar } = useProps(props); const { dataSource, fieldNames, showLunar } = useProps(props);
const height = useCalenderHeight(); const height = useCalenderHeight();
const [date, setDate] = useState<Date>(new Date()); const [date, setDate] = useState<Date>(new Date());
const [view, setView] = useState<View>('month'); const [view, setView] = useState<View>('month');
const events = useEvents(dataSource, fieldNames, date, view); const events = useEvents(dataSource, fieldNames, date, view);
const [visible, setVisible] = useState(false);
const [record, setRecord] = useState<any>({}); const [record, setRecord] = useState<any>({});
const { wrapSSR, hashId, componentCls: containerClassName } = useStyle(); const { wrapSSR, hashId, componentCls: containerClassName } = useStyle();
const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema();
const components = useMemo(() => { const components = useMemo(() => {
return { return {
@ -247,8 +218,11 @@ export const Calendar: any = withDynamicSchemaProps(
}; };
return wrapSSR( return wrapSSR(
<div className={`${hashId} ${containerClassName}`} style={{ height: height || 700 }}> <div className={`${hashId} ${containerClassName}`} style={{ height: height || 700 }}>
<PopupContextProvider visible={visible} setVisible={setVisible}>
<GlobalStyle /> <GlobalStyle />
<CalendarRecordViewer visible={visible} setVisible={setVisible} record={record} /> <RecordProvider record={record} parent={parentRecordData}>
<CalendarRecordViewer />
</RecordProvider>
<BigCalendar <BigCalendar
popup popup
selectable selectable
@ -275,7 +249,10 @@ export const Calendar: any = withDynamicSchemaProps(
record.__event = { ...event, start: formatDate(dayjs(event.start)), end: formatDate(dayjs(event.end)) }; record.__event = { ...event, start: formatDate(dayjs(event.start)), end: formatDate(dayjs(event.end)) };
setRecord(record); setRecord(record);
setVisible(true); openPopup({
recordData: record,
customActionSchema: findEventSchema(fieldSchema),
});
}} }}
formats={{ formats={{
monthHeaderFormat: 'YYYY-M', monthHeaderFormat: 'YYYY-M',
@ -291,6 +268,7 @@ export const Calendar: any = withDynamicSchemaProps(
components={components} components={components}
localizer={localizer} localizer={localizer}
/> />
</PopupContextProvider>
</div>, </div>,
); );
}, },

View File

@ -0,0 +1,35 @@
/**
* 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 { RecursionField, Schema, useFieldSchema } from '@formily/react';
import React, { FC, useMemo } from 'react';
export const CalendarRecordViewer: FC = (props) => {
const fieldSchema = useFieldSchema();
const eventSchema: Schema = useMemo(() => findEventSchema(fieldSchema), [fieldSchema]);
if (!eventSchema) {
return null;
}
return <RecursionField schema={eventSchema} name={eventSchema.name} />;
};
export function findEventSchema(schema: Schema) {
if (schema['x-component'].endsWith('.Event')) {
return schema;
}
return schema.reduceProperties((buf, current) => {
if (current['x-component'].endsWith('.Event')) {
return current;
}
return buf;
}, null);
}

View File

@ -8,11 +8,35 @@
*/ */
import { observer } from '@formily/react'; import { observer } from '@formily/react';
import React from 'react'; import {
PopupContextProvider,
useActionContext,
useCollection,
useCollectionRecordData,
VariablePopupRecordProvider,
} from '@nocobase/client';
import React, { useCallback } from 'react';
import { DeleteEventContext } from './Calendar';
export const Event = observer( export const Event = observer(
(props) => { (props) => {
return <>{props.children}</>; const { visible, setVisible } = useActionContext();
const recordData = useCollectionRecordData();
const collection = useCollection();
const close = useCallback(() => {
setVisible(false);
}, [setVisible]);
return (
<PopupContextProvider visible={visible} setVisible={setVisible}>
<DeleteEventContext.Provider value={{ close }}>
<VariablePopupRecordProvider recordData={recordData} collection={collection}>
{props.children}
</VariablePopupRecordProvider>
</DeleteEventContext.Provider>
</PopupContextProvider>
);
}, },
{ displayName: 'Event' }, { displayName: 'Event' },
); );

View File

@ -8,11 +8,28 @@
*/ */
import { observer } from '@formily/react'; import { observer } from '@formily/react';
import {
PopupContextProvider,
useActionContext,
useCollection,
useCollectionRecordData,
VariablePopupRecordProvider,
} from '@nocobase/client';
import React from 'react'; import React from 'react';
export const Event = observer( export const Event = observer(
(props) => { (props) => {
return <>{props.children}</>; const { visible, setVisible } = useActionContext();
const recordData = useCollectionRecordData();
const collection = useCollection();
return (
<PopupContextProvider visible={visible} setVisible={setVisible}>
<VariablePopupRecordProvider recordData={recordData} collection={collection}>
{props.children}
</VariablePopupRecordProvider>
</PopupContextProvider>
);
}, },
{ displayName: 'Event' }, { displayName: 'Event' },
); );

View File

@ -0,0 +1,23 @@
/**
* 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 { RecursionField, useFieldSchema } from '@formily/react';
import { Schema } from '@nocobase/utils';
import React, { FC } from 'react';
export const GanttRecordViewer: FC = (props) => {
const fieldSchema = useFieldSchema();
const eventSchema: Schema = fieldSchema.properties.detail;
if (!eventSchema) {
return null;
}
return <RecursionField schema={eventSchema} name={eventSchema.name} />;
};

View File

@ -8,17 +8,16 @@
*/ */
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { RecursionField, Schema, useFieldSchema } from '@formily/react'; import { RecursionField, useFieldSchema } from '@formily/react';
import { import {
ActionContextProvider, PopupContextProvider,
RecordProvider, RecordProvider,
VariablePopupRecordProvider,
useAPIClient, useAPIClient,
useBlockRequestContext, useBlockRequestContext,
useCollection,
useCollectionParentRecordData, useCollectionParentRecordData,
useCurrentAppInfo, useCurrentAppInfo,
useDesignable, useDesignable,
usePopupUtils,
useProps, useProps,
useTableBlockContext, useTableBlockContext,
useToken, useToken,
@ -26,7 +25,7 @@ import {
} from '@nocobase/client'; } from '@nocobase/client';
import { Spin, message } from 'antd'; import { Spin, message } from 'antd';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import React, { SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { SyntheticEvent, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useGanttBlockContext } from '../../GanttBlockProvider'; import { useGanttBlockContext } from '../../GanttBlockProvider';
import { convertToBarTasks } from '../../helpers/bar-helper'; import { convertToBarTasks } from '../../helpers/bar-helper';
@ -41,6 +40,7 @@ import { GridProps } from '../grid/grid';
import { HorizontalScroll } from '../other/horizontal-scroll'; import { HorizontalScroll } from '../other/horizontal-scroll';
import { StandardTooltipContent, Tooltip } from '../other/tooltip'; import { StandardTooltipContent, Tooltip } from '../other/tooltip';
import { VerticalScroll } from '../other/vertical-scroll'; import { VerticalScroll } from '../other/vertical-scroll';
import { GanttRecordViewer } from './GanttRecordViewer';
import useStyles from './style'; import useStyles from './style';
import { TaskGantt } from './task-gantt'; import { TaskGantt } from './task-gantt';
import { TaskGanttContentProps } from './task-gantt-content'; import { TaskGanttContentProps } from './task-gantt-content';
@ -49,34 +49,7 @@ const getColumnWidth = (dataSetLength: any, clientWidth: any) => {
const columnWidth = clientWidth / dataSetLength > 50 ? Math.floor(clientWidth / dataSetLength) + 20 : 50; const columnWidth = clientWidth / dataSetLength > 50 ? Math.floor(clientWidth / dataSetLength) + 20 : 50;
return columnWidth; return columnWidth;
}; };
export const DeleteEventContext = React.createContext({
close: () => {},
});
const GanttRecordViewer = (props) => {
const { visible, setVisible, record } = props;
const { t } = useTranslation();
const collection = useCollection();
const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema();
const eventSchema: Schema = fieldSchema.properties.detail;
const close = useCallback(() => {
setVisible(false);
}, []);
return (
eventSchema && (
<DeleteEventContext.Provider value={{ close }}>
<ActionContextProvider value={{ visible, setVisible }}>
<RecordProvider record={record} parent={parentRecordData}>
<VariablePopupRecordProvider recordData={record} collection={collection}>
<RecursionField schema={eventSchema} name={eventSchema.name} />
</VariablePopupRecordProvider>
</RecordProvider>
</ActionContextProvider>
</DeleteEventContext.Provider>
)
);
};
const debounceHandleTaskChange = debounce(async (task: Task, resource, fieldNames, service, t) => { const debounceHandleTaskChange = debounce(async (task: Task, resource, fieldNames, service, t) => {
await resource.update({ await resource.update({
filterByTk: task.id, filterByTk: task.id,
@ -160,7 +133,11 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
return { viewMode, dates: seedDates(startDate, endDate, viewMode) }; return { viewMode, dates: seedDates(startDate, endDate, viewMode) };
}); });
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { openPopup } = usePopupUtils({
setVisible,
});
const [record, setRecord] = useState<any>({}); const [record, setRecord] = useState<any>({});
const parentRecordData = useCollectionParentRecordData();
const [currentViewDate, setCurrentViewDate] = useState<Date | undefined>(undefined); const [currentViewDate, setCurrentViewDate] = useState<Date | undefined>(undefined);
const [taskListWidth, setTaskListWidth] = useState(0); const [taskListWidth, setTaskListWidth] = useState(0);
const [svgContainerWidth, setSvgContainerWidth] = useState(0); const [svgContainerWidth, setSvgContainerWidth] = useState(0);
@ -473,7 +450,10 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
return; return;
} }
setRecord(recordData); setRecord(recordData);
setVisible(true); openPopup({
recordData,
customActionSchema: fieldSchema.properties.detail,
});
}; };
const gridProps: GridProps = { const gridProps: GridProps = {
columnWidth, columnWidth,
@ -536,7 +516,10 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
`)} `)}
ref={ganttRef} ref={ganttRef}
> >
<GanttRecordViewer visible={visible} setVisible={setVisible} record={record} /> <PopupContextProvider visible={visible} setVisible={setVisible}>
<RecordProvider record={record} parent={parentRecordData}>
<GanttRecordViewer />
</RecordProvider>
<RecursionField name={'anctionBar'} schema={fieldSchema.properties.toolBar} /> <RecursionField name={'anctionBar'} schema={fieldSchema.properties.toolBar} />
<RecursionField name={'table'} schema={fieldSchema.properties.table} /> <RecursionField name={'table'} schema={fieldSchema.properties.table} />
<div className={styles.wrapper} onKeyDown={handleKeyDown} tabIndex={0} ref={wrapperRef}> <div className={styles.wrapper} onKeyDown={handleKeyDown} tabIndex={0} ref={wrapperRef}>
@ -585,6 +568,7 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
/> />
</Spin> </Spin>
</div> </div>
</PopupContextProvider>
</div> </div>
); );
}); });

View File

@ -8,15 +8,29 @@
*/ */
import { import {
PopupContextProvider,
useCollection_deprecated, useCollection_deprecated,
useCollectionManager_deprecated, useCollectionManager_deprecated,
usePopupUtils,
useProps, useProps,
withDynamicSchemaProps, withDynamicSchemaProps,
} from '@nocobase/client'; } from '@nocobase/client';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { MapBlockComponent } from '../components'; import { MapBlockComponent } from '../components';
import { MapBlockDrawer } from '../components/MapBlockDrawer';
export const MapBlock = withDynamicSchemaProps((props) => { export const MapBlock = withDynamicSchemaProps((props) => {
const { context } = usePopupUtils();
// only render the popup
if (context.currentLevel) {
return (
<PopupContextProvider>
<MapBlockDrawer />
</PopupContextProvider>
);
}
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { fieldNames } = useProps(props); const { fieldNames } = useProps(props);

View File

@ -8,11 +8,8 @@
*/ */
import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons'; import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons';
import { RecursionField, useFieldSchema } from '@formily/react';
import { import {
ActionContextProvider,
RecordProvider, RecordProvider,
VariablePopupRecordProvider,
css, css,
getLabelFormatValue, getLabelFormatValue,
useCollection, useCollection,
@ -21,14 +18,16 @@ import {
useCollection_deprecated, useCollection_deprecated,
useCompile, useCompile,
useFilterAPI, useFilterAPI,
usePopupUtils,
useProps, useProps,
} from '@nocobase/client'; } from '@nocobase/client';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Button, Space } from 'antd'; import { Button, Space } from 'antd';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { defaultImage, selectedImage } from '../../constants'; import { defaultImage, selectedImage } from '../../constants';
import { useMapTranslation } from '../../locale'; import { useMapTranslation } from '../../locale';
import { getSource } from '../../utils'; import { getSource } from '../../utils';
import { MapBlockDrawer } from '../MapBlockDrawer';
import { AMapComponent, AMapForwardedRefProps } from './Map'; import { AMapComponent, AMapForwardedRefProps } from './Map';
export const AMapBlock = (props) => { export const AMapBlock = (props) => {
@ -50,6 +49,9 @@ export const AMapBlock = (props) => {
const selectingModeRef = useRef(selectingMode); const selectingModeRef = useRef(selectingMode);
selectingModeRef.current = selectingMode; selectingModeRef.current = selectingMode;
const { fields } = useCollection(); const { fields } = useCollection();
const parentRecordData = useCollectionParentRecordData();
const { openPopup } = usePopupUtils();
const labelUiSchema = fields.find((v) => v.name === fieldNames?.marker)?.uiSchema; const labelUiSchema = fields.find((v) => v.name === fieldNames?.marker)?.uiSchema;
const setOverlayOptions = (overlay: AMap.Polygon | AMap.Marker, state?: boolean) => { const setOverlayOptions = (overlay: AMap.Polygon | AMap.Marker, state?: boolean) => {
const extData = overlay.getExtData(); const extData = overlay.getExtData();
@ -199,6 +201,9 @@ export const AMapBlock = (props) => {
if (data) { if (data) {
setRecord(data); setRecord(data);
openPopup({
recordData: data,
});
} }
}; };
o.on('click', onClick); o.on('click', onClick);
@ -244,7 +249,17 @@ export const AMapBlock = (props) => {
}); });
events.forEach((e) => e()); events.forEach((e) => e());
}; };
}, [dataSource, isMapInitialization, fieldNames, name, primaryKey, collectionField.type, isConnected, lineSort]); }, [
dataSource,
isMapInitialization,
fieldNames,
name,
primaryKey,
collectionField.type,
isConnected,
lineSort,
openPopup,
]);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
@ -307,7 +322,9 @@ export const AMapBlock = (props) => {
</Space> </Space>
) : null} ) : null}
</div> </div>
<MapBlockDrawer record={record} setVisible={setRecord} /> <RecordProvider record={record} parent={parentRecordData}>
<MapBlockDrawer />
</RecordProvider>
<AMapComponent <AMapComponent
{...collectionField?.uiSchema?.['x-component-props']} {...collectionField?.uiSchema?.['x-component-props']}
ref={mapRefCallback} ref={mapRefCallback}
@ -324,35 +341,6 @@ export const AMapBlock = (props) => {
); );
}; };
const MapBlockDrawer = (props) => {
const { setVisible, record } = props;
const collection = useCollection();
const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema();
const schema = useMemo(
() =>
fieldSchema.reduceProperties((buf, current) => {
if (current.name === 'drawer') {
return current;
}
return buf;
}, null),
[fieldSchema],
);
return (
schema && (
<ActionContextProvider value={{ visible: !!record, setVisible }}>
<RecordProvider record={record} parent={parentRecordData}>
<VariablePopupRecordProvider recordData={record} collection={collection}>
<RecursionField schema={schema} name={schema.name} />
</VariablePopupRecordProvider>
</RecordProvider>
</ActionContextProvider>
)
);
};
function clearSelected(marker: AMap.Marker | AMap.Polygon | AMap.Polyline | AMap.Circle) { function clearSelected(marker: AMap.Marker | AMap.Polygon | AMap.Polyline | AMap.Circle) {
if ((marker as AMap.Marker).dom) { if ((marker as AMap.Marker).dom) {
(marker as AMap.Marker).dom.style.filter = 'none'; (marker as AMap.Marker).dom.style.filter = 'none';

View File

@ -8,11 +8,8 @@
*/ */
import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons'; import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons';
import { RecursionField, Schema, useFieldSchema } from '@formily/react';
import { import {
ActionContextProvider,
RecordProvider, RecordProvider,
VariablePopupRecordProvider,
css, css,
getLabelFormatValue, getLabelFormatValue,
useCollection, useCollection,
@ -21,14 +18,16 @@ import {
useCollection_deprecated, useCollection_deprecated,
useCompile, useCompile,
useFilterAPI, useFilterAPI,
usePopupUtils,
useProps, useProps,
} from '@nocobase/client'; } from '@nocobase/client';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Button, Space } from 'antd'; import { Button, Space } from 'antd';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { defaultImage, selectedImage } from '../../constants'; import { defaultImage, selectedImage } from '../../constants';
import { useMapTranslation } from '../../locale'; import { useMapTranslation } from '../../locale';
import { getSource } from '../../utils'; import { getSource } from '../../utils';
import { MapBlockDrawer } from '../MapBlockDrawer';
import { GoogleMapForwardedRefProps, GoogleMapsComponent, OverlayOptions } from './Map'; import { GoogleMapForwardedRefProps, GoogleMapsComponent, OverlayOptions } from './Map';
import { getIcon } from './utils'; import { getIcon } from './utils';
@ -69,8 +68,10 @@ export const GoogleMapsBlock = (props) => {
const overlaysRef = useRef<google.maps.MVCObject[]>([]); const overlaysRef = useRef<google.maps.MVCObject[]>([]);
selectingModeRef.current = selectingMode; selectingModeRef.current = selectingMode;
const { fields } = useCollection(); const { fields } = useCollection();
const parentRecordData = useCollectionParentRecordData();
const labelUiSchema = fields.find((v) => v.name === fieldNames?.marker)?.uiSchema; const labelUiSchema = fields.find((v) => v.name === fieldNames?.marker)?.uiSchema;
const { getCollectionJoinField } = useCollectionManager_deprecated(); const { getCollectionJoinField } = useCollectionManager_deprecated();
const { openPopup } = usePopupUtils();
const setOverlayOptions = (overlay: google.maps.MVCObject, state?: boolean) => { const setOverlayOptions = (overlay: google.maps.MVCObject, state?: boolean) => {
const selected = typeof state !== 'undefined' ? !state : overlay.get(OVERLAY_SELECtED); const selected = typeof state !== 'undefined' ? !state : overlay.get(OVERLAY_SELECtED);
@ -234,6 +235,9 @@ export const GoogleMapsBlock = (props) => {
if (data) { if (data) {
setRecord(data); setRecord(data);
openPopup({
recordData: data,
});
} }
}; };
o.addListener('click', onClick); o.addListener('click', onClick);
@ -291,7 +295,7 @@ export const GoogleMapsBlock = (props) => {
}); });
events.forEach((e) => e()); events.forEach((e) => e());
}; };
}, [dataSource, isMapInitialization, markerName, collectionField.type, isConnected]); }, [dataSource, isMapInitialization, markerName, collectionField.type, isConnected, openPopup]);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
@ -354,7 +358,9 @@ export const GoogleMapsBlock = (props) => {
) : null} ) : null}
</Space> </Space>
</div> </div>
<MapBlockDrawer record={record} setVisible={setRecord} /> <RecordProvider record={record} parent={parentRecordData}>
<MapBlockDrawer />
</RecordProvider>
</> </>
)} )}
<GoogleMapsComponent <GoogleMapsComponent
@ -373,35 +379,6 @@ export const GoogleMapsBlock = (props) => {
); );
}; };
const MapBlockDrawer = (props) => {
const { setVisible, record } = props;
const collection = useCollection();
const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema();
const schema: Schema = useMemo(
() =>
fieldSchema.reduceProperties((buf, current) => {
if (current.name === 'drawer') {
return current;
}
return buf;
}, null),
[fieldSchema],
);
return (
schema && (
<ActionContextProvider value={{ visible: !!record, setVisible }}>
<RecordProvider record={record} parent={parentRecordData}>
<VariablePopupRecordProvider recordData={record} collection={collection}>
<RecursionField schema={schema} name={schema.name} />
</VariablePopupRecordProvider>
</RecordProvider>
</ActionContextProvider>
)
);
};
function clearSelected(target: google.maps.Polygon) { function clearSelected(target: google.maps.Polygon) {
if (target instanceof google.maps.Marker) { if (target instanceof google.maps.Marker) {
return target.setIcon(getIcon(defaultImage)); return target.setIcon(getIcon(defaultImage));

View File

@ -7,6 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { PopupContextProvider } from '@nocobase/client';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useMapTranslation } from '../locale'; import { useMapTranslation } from '../locale';
import { AMapBlock } from './AMap'; import { AMapBlock } from './AMap';
@ -29,5 +30,9 @@ export const MapBlockComponent: React.FC<any> = (props) => {
return <div>{t(`The ${mapType} cannot found`)}</div>; return <div>{t(`The ${mapType} cannot found`)}</div>;
} }
return <Component {...props} />; return (
<PopupContextProvider>
<Component {...props} />
</PopupContextProvider>
);
}; };

View File

@ -0,0 +1,38 @@
/**
* 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 { RecursionField, useFieldSchema } from '@formily/react';
import { useCollection, useCollectionRecordData, VariablePopupRecordProvider } from '@nocobase/client';
import React, { FC, useMemo } from 'react';
export const MapBlockDrawer: FC = (props) => {
const recordData = useCollectionRecordData();
const collection = useCollection();
const fieldSchema = useFieldSchema();
const schema = useMemo(
() =>
fieldSchema.reduceProperties((buf, current) => {
if (current.name === 'drawer') {
return current;
}
return buf;
}, null),
[fieldSchema],
);
if (!schema) {
return null;
}
return (
<VariablePopupRecordProvider recordData={recordData} collection={collection}>
<RecursionField schema={schema} name={schema.name} />
</VariablePopupRecordProvider>
);
};