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
.reset()
.then(() => {
ctx.form.setInitialValues(data || {});
ctx.form.setValues(data || {});
})
.catch(console.error);

View File

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

View File

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

View File

@ -7,13 +7,31 @@
* 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.
* @returns The popup settings.
*/
export const usePopupSettings = () => {
const { enableURL } = React.useContext(PopupSettingsContext);
const isPopupVisibleControlledByURL = useCallback(() => {
const pathname = window.location.pathname;
const hash = window.location.hash;
@ -21,8 +39,8 @@ export const usePopupSettings = () => {
const isNewMobileMode = pathname?.includes('/m/');
const isPCMode = pathname?.includes('/admin/');
return (isPCMode || isNewMobileMode) && !isOldMobileMode;
}, []);
return (isPCMode || isNewMobileMode) && !isOldMobileMode && enableURL;
}, [enableURL]);
return {
/** 弹窗窗口的显隐是否由 URL 控制 */

View File

@ -15,3 +15,4 @@ export * from './Page.Settings';
export { PagePopups } from './PagePopups';
export { storePopupContext } from './pagePopupUtils';
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
* @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 location = useLocationNoUpdate();
const fieldSchema = useFieldSchema();
@ -141,14 +150,16 @@ export const usePopupUtils = () => {
const { params: popupParams } = useCurrentPopupContext();
const service = useDataBlockRequest();
const { isPopupVisibleControlledByURL } = usePopupSettings();
const { setVisible: setVisibleFromAction } = useContext(ActionContext);
const { setVisible: _setVisibleFromAction } = useContext(ActionContext);
const { updatePopupContext } = usePopupContextInActionOrAssociationField();
const currentPopupContext = useCurrentPopupContext();
const getSourceId = useCallback(
(_parentRecordData?: Record<string, any>) =>
(_parentRecordData || parentRecord?.data)?.[cm.getSourceKeyByAssociation(association)],
[parentRecord, association],
);
const currentPopupUidWithoutOpened = fieldSchema?.['x-uid'];
const setVisibleFromAction = options.setVisible || _setVisibleFromAction;
const getNewPathname = useCallback(
({
@ -199,6 +210,7 @@ export const usePopupUtils = () => {
parentRecordData,
collectionNameUsedInURL,
popupUidUsedInURL,
customActionSchema,
}: {
recordData?: Record<string, any>;
parentRecordData?: Record<string, any>;
@ -206,11 +218,13 @@ export const usePopupUtils = () => {
collectionNameUsedInURL?: string;
/** if this value exists, it will be saved in the URL */
popupUidUsedInURL?: string;
customActionSchema?: ISchema;
} = {}) => {
if (!isPopupVisibleControlledByURL()) {
return setVisibleFromAction?.(true);
}
const currentPopupUidWithoutOpened = customActionSchema?.['x-uid'] || fieldSchema?.['x-uid'];
const sourceId = getSourceId(parentRecordData);
recordData = recordData || record?.data;
@ -227,7 +241,7 @@ export const usePopupUtils = () => {
}
storePopupContext(currentPopupUidWithoutOpened, {
schema: fieldSchema,
schema: customActionSchema || fieldSchema,
record: new CollectionRecord({ isNew: false, data: recordData }),
parentRecord: parentRecordData ? new CollectionRecord({ isNew: false, data: parentRecordData }) : parentRecord,
service,
@ -237,7 +251,7 @@ export const usePopupUtils = () => {
sourceId,
});
updatePopupContext(getPopupContext());
updatePopupContext(getPopupContext(), customActionSchema);
navigate(withSearchParams(`${url}${pathname}`));
},
@ -256,7 +270,6 @@ export const usePopupUtils = () => {
isPopupVisibleControlledByURL,
getSourceId,
getPopupContext,
currentPopupUidWithoutOpened,
],
);
@ -317,6 +330,7 @@ export const usePopupUtils = () => {
closePopup,
savePopupSchemaToSchema,
getPopupSchemaFromSchema,
context: currentPopupContext,
/**
* @deprecated
* TODO: remove this

View File

@ -29,18 +29,19 @@ export const usePopupContextInActionOrAssociationField = () => {
const { dn } = useDesignable();
const updatePopupContext = useCallback(
(context: PopupContext) => {
(context: PopupContext, customSchema?: ISchema) => {
customSchema = customSchema || fieldSchema;
context = _.omitBy(context, _.isNil) as PopupContext;
if (_.isEqual(context, getPopupContextFromActionOrAssociationFieldSchema(fieldSchema))) {
if (_.isEqual(context, getPopupContextFromActionOrAssociationFieldSchema(customSchema))) {
return;
}
fieldSchema[CONTEXT_SCHEMA_KEY] = context;
customSchema[CONTEXT_SCHEMA_KEY] = context;
return dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-uid': customSchema['x-uid'],
[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 { bulkEditActionSettings, deprecatedBulkEditActionSettings } from './BulkEditAction.Settings';
import { BulkEditActionDecorator } from './BulkEditActionDecorator';
import { BulkEditActionInitializer } from './BulkEditActionInitializer';
import {
BulkEditBlockInitializers_deprecated,
@ -25,7 +26,7 @@ import { BulkEditField } from './component/BulkEditField';
import { useCustomizeBulkEditActionProps } from './utils';
export class PluginActionBulkEditClient extends Plugin {
async load() {
this.app.addComponents({ BulkEditField });
this.app.addComponents({ BulkEditField, BulkEditActionDecorator });
this.app.addScopes({ useCustomizeBulkEditActionProps });
this.app.schemaSettingsManager.add(deprecatedBulkEditActionSettings);
this.app.schemaSettingsManager.add(bulkEditActionSettings);
@ -45,7 +46,7 @@ export class PluginActionBulkEditClient extends Plugin {
Component: BulkEditActionInitializer,
schema: {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',
'x-decorator': 'BulkEditActionDecorator',
'x-action': 'customize:bulkEdit',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:bulkEdit',

View File

@ -13,6 +13,7 @@ import {
ActionContextProvider,
CollectionProvider_deprecated,
FormBlockContext,
PopupSettingsProvider,
RecordProvider,
fetchTemplateData,
useACLActionParamsContext,
@ -203,7 +204,9 @@ export const DuplicateAction = observer(
{/* 这里的 record 就是弹窗中创建表单的 sourceRecord */}
<RecordProvider record={{ ...parentRecordData, __collection: duplicateCollection || __collection }}>
<ActionContextProvider value={{ ...ctx, visible, setVisible }}>
<RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties />
<PopupSettingsProvider enableURL={false}>
<RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties />
</PopupSettingsProvider>
</ActionContextProvider>
</RecordProvider>
</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',
title: '{{ t("Duplicate") }}',
'x-component': 'Action.Link',
'x-decorator': 'ACLActionProvider',
'x-decorator': 'DuplicateActionDecorator',
'x-component-props': {
openMode: defaultOpenMode,
component: 'DuplicateAction',

View File

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

View File

@ -10,12 +10,12 @@
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { RecursionField, Schema, observer, useFieldSchema } from '@formily/react';
import {
ActionContextProvider,
PopupContextProvider,
RecordProvider,
VariablePopupRecordProvider,
getLabelFormatValue,
useCollection,
useCollectionParentRecordData,
usePopupUtils,
useProps,
withDynamicSchemaProps,
} from '@nocobase/client';
@ -23,10 +23,11 @@ import { parseExpression } from 'cron-parser';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
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 * as dates from 'react-big-calendar/lib/utils/dates';
import { i18nt, useTranslation } from '../../locale';
import { CalendarRecordViewer, findEventSchema } from './CalendarRecordViewer';
import Header from './components/Header';
import { CalendarToolbarContext } from './context';
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]);
};
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(
observer(
(props: any) => {
const [visible, setVisible] = useState(false);
const { openPopup } = usePopupUtils({
setVisible,
});
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { dataSource, fieldNames, showLunar } = useProps(props);
const height = useCalenderHeight();
const [date, setDate] = useState<Date>(new Date());
const [view, setView] = useState<View>('month');
const events = useEvents(dataSource, fieldNames, date, view);
const [visible, setVisible] = useState(false);
const [record, setRecord] = useState<any>({});
const { wrapSSR, hashId, componentCls: containerClassName } = useStyle();
const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema();
const components = useMemo(() => {
return {
@ -247,50 +218,57 @@ export const Calendar: any = withDynamicSchemaProps(
};
return wrapSSR(
<div className={`${hashId} ${containerClassName}`} style={{ height: height || 700 }}>
<GlobalStyle />
<CalendarRecordViewer visible={visible} setVisible={setVisible} record={record} />
<BigCalendar
popup
selectable
events={events}
view={view}
views={Weeks}
date={date}
step={60}
showMultiDayTimes
messages={messages}
onNavigate={setDate}
onView={setView}
onSelectSlot={(slotInfo) => {
console.log('onSelectSlot', slotInfo);
}}
onDoubleClickEvent={() => {
console.log('onDoubleClickEvent');
}}
onSelectEvent={(event) => {
const record = dataSource?.find((item) => item[fieldNames.id] === event.id);
if (!record) {
return;
}
record.__event = { ...event, start: formatDate(dayjs(event.start)), end: formatDate(dayjs(event.end)) };
setRecord(record);
setVisible(true);
}}
formats={{
monthHeaderFormat: 'YYYY-M',
agendaDateFormat: 'M-DD',
dayHeaderFormat: 'YYYY-M-DD',
dayRangeHeaderFormat: ({ start, end }, culture, local) => {
if (dates.eq(start, end, 'month')) {
return local.format(start, 'YYYY-M', culture);
<PopupContextProvider visible={visible} setVisible={setVisible}>
<GlobalStyle />
<RecordProvider record={record} parent={parentRecordData}>
<CalendarRecordViewer />
</RecordProvider>
<BigCalendar
popup
selectable
events={events}
view={view}
views={Weeks}
date={date}
step={60}
showMultiDayTimes
messages={messages}
onNavigate={setDate}
onView={setView}
onSelectSlot={(slotInfo) => {
console.log('onSelectSlot', slotInfo);
}}
onDoubleClickEvent={() => {
console.log('onDoubleClickEvent');
}}
onSelectEvent={(event) => {
const record = dataSource?.find((item) => item[fieldNames.id] === event.id);
if (!record) {
return;
}
return `${local.format(start, 'YYYY-M', culture)} - ${local.format(end, 'YYYY-M', culture)}`;
},
}}
components={components}
localizer={localizer}
/>
record.__event = { ...event, start: formatDate(dayjs(event.start)), end: formatDate(dayjs(event.end)) };
setRecord(record);
openPopup({
recordData: record,
customActionSchema: findEventSchema(fieldSchema),
});
}}
formats={{
monthHeaderFormat: 'YYYY-M',
agendaDateFormat: 'M-DD',
dayHeaderFormat: 'YYYY-M-DD',
dayRangeHeaderFormat: ({ start, end }, culture, local) => {
if (dates.eq(start, end, 'month')) {
return local.format(start, 'YYYY-M', culture);
}
return `${local.format(start, 'YYYY-M', culture)} - ${local.format(end, 'YYYY-M', culture)}`;
},
}}
components={components}
localizer={localizer}
/>
</PopupContextProvider>
</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 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(
(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' },
);

View File

@ -8,11 +8,28 @@
*/
import { observer } from '@formily/react';
import {
PopupContextProvider,
useActionContext,
useCollection,
useCollectionRecordData,
VariablePopupRecordProvider,
} from '@nocobase/client';
import React from 'react';
export const Event = observer(
(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' },
);

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 { RecursionField, Schema, useFieldSchema } from '@formily/react';
import { RecursionField, useFieldSchema } from '@formily/react';
import {
ActionContextProvider,
PopupContextProvider,
RecordProvider,
VariablePopupRecordProvider,
useAPIClient,
useBlockRequestContext,
useCollection,
useCollectionParentRecordData,
useCurrentAppInfo,
useDesignable,
usePopupUtils,
useProps,
useTableBlockContext,
useToken,
@ -26,7 +25,7 @@ import {
} from '@nocobase/client';
import { Spin, message } from 'antd';
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 { useGanttBlockContext } from '../../GanttBlockProvider';
import { convertToBarTasks } from '../../helpers/bar-helper';
@ -41,6 +40,7 @@ import { GridProps } from '../grid/grid';
import { HorizontalScroll } from '../other/horizontal-scroll';
import { StandardTooltipContent, Tooltip } from '../other/tooltip';
import { VerticalScroll } from '../other/vertical-scroll';
import { GanttRecordViewer } from './GanttRecordViewer';
import useStyles from './style';
import { TaskGantt } from './task-gantt';
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;
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) => {
await resource.update({
filterByTk: task.id,
@ -160,7 +133,11 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
return { viewMode, dates: seedDates(startDate, endDate, viewMode) };
});
const [visible, setVisible] = useState(false);
const { openPopup } = usePopupUtils({
setVisible,
});
const [record, setRecord] = useState<any>({});
const parentRecordData = useCollectionParentRecordData();
const [currentViewDate, setCurrentViewDate] = useState<Date | undefined>(undefined);
const [taskListWidth, setTaskListWidth] = useState(0);
const [svgContainerWidth, setSvgContainerWidth] = useState(0);
@ -473,7 +450,10 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
return;
}
setRecord(recordData);
setVisible(true);
openPopup({
recordData,
customActionSchema: fieldSchema.properties.detail,
});
};
const gridProps: GridProps = {
columnWidth,
@ -536,55 +516,59 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
`)}
ref={ganttRef}
>
<GanttRecordViewer visible={visible} setVisible={setVisible} record={record} />
<RecursionField name={'anctionBar'} schema={fieldSchema.properties.toolBar} />
<RecursionField name={'table'} schema={fieldSchema.properties.table} />
<div className={styles.wrapper} onKeyDown={handleKeyDown} tabIndex={0} ref={wrapperRef}>
<TaskGantt
gridProps={gridProps}
calendarProps={calendarProps}
barProps={barProps}
ganttHeight={ganttHeight}
scrollY={scrollY}
scrollX={scrollX}
ref={verticalGanttContainerRef}
/>
{ganttEvent.changedTask && (
<Tooltip
arrowIndent={arrowIndent}
rowHeight={rowHeight}
svgContainerHeight={svgContainerHeight}
svgContainerWidth={svgContainerWidth}
fontFamily={fontFamily}
fontSize={fontSize}
scrollX={scrollX}
<PopupContextProvider visible={visible} setVisible={setVisible}>
<RecordProvider record={record} parent={parentRecordData}>
<GanttRecordViewer />
</RecordProvider>
<RecursionField name={'anctionBar'} schema={fieldSchema.properties.toolBar} />
<RecursionField name={'table'} schema={fieldSchema.properties.table} />
<div className={styles.wrapper} onKeyDown={handleKeyDown} tabIndex={0} ref={wrapperRef}>
<TaskGantt
gridProps={gridProps}
calendarProps={calendarProps}
barProps={barProps}
ganttHeight={ganttHeight}
scrollY={scrollY}
task={ganttEvent.changedTask}
scrollX={scrollX}
ref={verticalGanttContainerRef}
/>
{ganttEvent.changedTask && (
<Tooltip
arrowIndent={arrowIndent}
rowHeight={rowHeight}
svgContainerHeight={svgContainerHeight}
svgContainerWidth={svgContainerWidth}
fontFamily={fontFamily}
fontSize={fontSize}
scrollX={scrollX}
scrollY={scrollY}
task={ganttEvent.changedTask}
headerHeight={headerHeight}
taskListWidth={taskListWidth}
TooltipContent={TooltipContent}
rtl={rtl}
svgWidth={svgWidth}
/>
)}
<VerticalScroll
ganttFullHeight={ganttFullHeight}
ganttHeight={ganttHeight}
headerHeight={headerHeight}
taskListWidth={taskListWidth}
TooltipContent={TooltipContent}
scroll={scrollY}
onScroll={handleScrollY}
rtl={rtl}
svgWidth={svgWidth}
/>
)}
<VerticalScroll
ganttFullHeight={ganttFullHeight}
ganttHeight={ganttHeight}
headerHeight={headerHeight}
scroll={scrollY}
onScroll={handleScrollY}
rtl={rtl}
/>
<Spin spinning={loading} style={{ visibility: 'hidden' }}>
<HorizontalScroll
svgWidth={svgWidth}
taskListWidth={taskListWidth}
scroll={scrollX}
rtl={rtl}
onScroll={handleScrollX}
/>
</Spin>
</div>
<Spin spinning={loading} style={{ visibility: 'hidden' }}>
<HorizontalScroll
svgWidth={svgWidth}
taskListWidth={taskListWidth}
scroll={scrollX}
rtl={rtl}
onScroll={handleScrollX}
/>
</Spin>
</div>
</PopupContextProvider>
</div>
);
});

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { PopupContextProvider } from '@nocobase/client';
import React, { useMemo } from 'react';
import { useMapTranslation } from '../locale';
import { AMapBlock } from './AMap';
@ -29,5 +30,9 @@ export const MapBlockComponent: React.FC<any> = (props) => {
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>
);
};