From a429b7a4b370ced102125ba710805d4735aa07f2 Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Wed, 7 Aug 2024 14:25:40 +0800 Subject: [PATCH] feat: adapt desktop blocks to mobile (#4945) * feat: register workflow blocks to mobile page * fix: should hide Divider in subpage * refactor: rename 'Data blocks' to 'Desktop data blocks' * feat: adapt blocks within subpages for mobile * feat: adapt Filter action * feat: isolate block templates between desktop and mobile * refactor: export storePopupContext * feat: support popup URL for 'Workflow todos' * chore: update e2e tests * chore: make e2e tests pass * chore: add comment * fix: make popup style of duplicate and bulk edit right * fix(GridCard): ensure single column display in mobile * fix: fix goBack * refactor: make more stable * refactor: change name for add blocks menu * fix: fix block template for mobile * feat: adapt Apply action of approval block to mobile * fix(Map): use window.open to redirect to configuration page * Revert "fix(Map): use window.open to redirect to configuration page" This reverts commit 248ae8b68cfd78415184dfab2442081363872fb0. * fix: redirect to the main app page when URL is starts with 'admin' * fix(Link): make path right * fix: refactor Popup to fix draging bug * fix: should auto refresh when submiting in Manual popup * fix(Action.Container): should return null when visible is false (T-4949) * fix: increase z-index of subpage to cover Amap elements * fix: fix tab switching not work (T-4985) * fix(Link): should be change Link's URL of all table rows after editing URL (T-4981) * fix: fix URL not changed after closing popup (T-4987) * fix: make unit tests pass * fix: make unit tests pass * chore: get e2e tests to pass * fix: use Popup to display data picker (T-4965) * fix: use mobile Popup in some bloks * refactor: use local isMobile * fix: increase Popup's z-index to cover subpage * fix: optimize Popup for mobile * style: createRecordAction style improve * refactor(AssociationField): get Component from AssociationFieldModeProvider * refactor(InternalPopoverNester): support custom Container component * feat: adapt PopoverNester to mobile * chore: update unit tests * fix: get e2e tests to pass * chore: make e2e more stable * refactor: move mobile-action-page in adaptor-of-desktop folder * fix: get the z-index of popups and subpages correct * feat: unify the styles of popups * chore: make e2e more stable --------- Co-authored-by: chenos Co-authored-by: katherinehhh --- lerna.json | 4 +- .../client/src/block-provider/hooks/index.ts | 15 +- .../hooks/useBlockHeightProps.tsx | 2 +- .../src/global-theme/AntdAppProvider.tsx | 12 +- .../add-new/createFormBlockInitializers.tsx | 2 +- .../link/customizeLinkActionSettings.tsx | 8 +- .../detailsWithPaginationSettings.tsx | 4 +- .../details-single/detailsBlockSettings.ts | 12 +- .../form/createFormBlockSettings.tsx | 7 +- .../data-blocks/form/editFormBlockSettings.ts | 7 +- .../grid-card/GridCardBlockInitializer.tsx | 2 +- .../grid-card/gridCardBlockSettings.ts | 5 +- .../data-blocks/list/ListBlockInitializer.tsx | 2 +- .../data-blocks/list/listBlockSettings.ts | 5 +- .../table/TableBlockInitializer.tsx | 2 +- .../data-blocks/table/tableBlockSettings.tsx | 5 +- .../FilterCollapseBlockInitializer.tsx | 6 +- .../collapse/filterCollapseBlockSettings.ts | 6 +- .../form/FilterFormBlockInitializer.tsx | 8 +- .../form/filterFormBlockSettings.ts | 9 +- .../antd/action/Action.Container.tsx | 4 +- .../schema-component/antd/action/context.tsx | 8 +- .../antd/action/demos/demo1.tsx | 14 +- .../antd/action/demos/demo2.tsx | 20 ++- .../antd/action/demos/demo4.tsx | 14 +- .../AssociationFieldModeProvider.tsx | 81 +++++++++ .../antd/association-field/Editable.tsx | 19 +- .../InternalPopoverNester.tsx | 33 ++-- .../antd/association-field/hooks.ts | 3 +- .../antd/association-field/index.ts | 1 + .../AssociationFilter.BlockDesigner.tsx | 8 +- .../association-select/AssociationSelect.tsx | 2 + .../antd/filter/FilterAction.tsx | 19 +- .../antd/filter/FilterItem.tsx | 20 ++- .../antd/filter/demos/demo5.tsx | 22 ++- .../antd/form-v2/Form.Settings.tsx | 15 +- .../antd/form/Form.Settings.tsx | 4 +- .../src/schema-component/antd/form/Form.tsx | 4 +- .../antd/form/demos/demo1.tsx | 22 ++- .../antd/form/demos/demo2.tsx | 15 +- .../antd/form/demos/demo3.tsx | 15 +- .../antd/form/demos/demo4.tsx | 22 ++- .../antd/form/demos/demo5.tsx | 24 +-- .../antd/form/demos/demo6.tsx | 15 +- .../antd/form/demos/demo7.tsx | 29 ++- .../antd/form/demos/demo8.tsx | 32 ++-- .../antd/grid-card/GridCard.Designer.tsx | 13 +- .../antd/list/List.Designer.tsx | 10 +- .../antd/page/BackButtonUsedInSubPage.tsx | 10 +- .../src/schema-component/antd/page/index.ts | 1 + .../antd/page/pagePopupUtils.tsx | 17 +- .../antd/record-picker/InputRecordPicker.tsx | 9 +- .../antd/table-v2/TableBlockDesigner.tsx | 8 +- .../antd/table/Table.Void.Designer.tsx | 4 +- .../buttons/RecordBlockInitializers.tsx | 8 +- .../buttons/TabPaneInitializers.tsx | 1 + .../components/CreateRecordAction.tsx | 2 +- .../client/src/schema-initializer/utils.ts | 54 ++++-- .../src/schema-settings/SchemaSettings.tsx | 2 +- .../SchemaSettingsTemplate.tsx | 2 +- .../src/schema-templates/BlockTemplate.tsx | 17 +- .../BlockTemplateProvider.tsx | 35 ++++ .../core/client/src/schema-templates/index.ts | 1 + packages/core/utils/src/client.ts | 4 +- .../src/client/__e2e__/menu.test.ts | 16 +- .../src/client/BulkEditActionInitializer.tsx | 6 +- .../src/client/DuplicateActionInitializer.tsx | 6 +- .../createCalendarBlockSchema.test.ts | 2 +- .../src/client/calendar/Calender.Settings.tsx | 4 +- .../createCalendarBlockUISchema.ts | 2 +- .../items/CalendarBlockInitializer.tsx | 2 +- .../src/client/Gantt.Settings.tsx | 7 +- .../src/client/GanttBlockInitializer.tsx | 18 +- .../src/client/Kanban.Settings.tsx | 19 +- .../src/client/KanbanBlockInitializer.tsx | 16 +- .../block/createMapBlockSchema.test.ts | 2 +- .../src/client/block/MapBlock.Settings.tsx | 6 +- .../src/client/block/MapBlockInitializer.tsx | 5 +- .../client/block/createMapBlockUISchema.ts | 3 +- .../src/client/components/AMap/Block.tsx | 1 - .../client/components/GoogleMaps/Block.tsx | 1 - .../adaptor-of-desktop/ActionDrawer.style.ts | 67 +++++++ .../adaptor-of-desktop/ActionDrawer.tsx | 127 ++++++++++++++ .../BasicZIndexProvider.tsx | 33 ++++ .../adaptor-of-desktop/FilterAction.tsx | 166 ++++++++++++++++++ .../InternalPopoverNester.style.ts | 77 ++++++++ .../InternalPopoverNester.tsx | 88 ++++++++++ .../ResetSchemaOptionsProvider.tsx | 25 +++ .../MobileActionPage.style.ts | 14 ++ .../mobile-action-page/MobileActionPage.tsx | 147 ++++++++++++++++ .../MobileTabsForMobileActionPage.style.ts | 0 .../MobileTabsForMobileActionPage.tsx | 40 ++++- .../useGridCardBlockDecoratorProps.tsx | 27 +++ .../plugin-mobile/src/client/index.tsx | 13 +- .../mobile-providers/MobileProviders.tsx | 1 + .../mobile-providers/context/MobileRoutes.tsx | 13 +- .../src/client/mobile/Mobile.tsx | 88 +++++++--- .../plugin-mobile/src/client/mobile/styles.ts | 13 ++ .../dynamic-page/content/initializer.tsx | 4 +- .../mobile-action-page/MobileActionPage.tsx | 60 ------- .../plugin-mobile/src/locale/en-US.json | 4 +- .../plugin-mobile/src/locale/zh-CN.json | 4 +- .../src/client/WorkflowManualProvider.tsx | 45 +++++ .../src/client/WorkflowTodo.tsx | 92 ++++++---- .../client/WorkflowTodoBlockInitializer.tsx | 1 - .../src/client/__e2e__/datablocks.test.ts | 2 +- .../src/client/index.ts | 9 + .../client/instruction/FormBlockProvider.tsx | 11 +- .../src/client/instruction/forms/custom.tsx | 6 +- 109 files changed, 1630 insertions(+), 449 deletions(-) create mode 100644 packages/core/client/src/schema-component/antd/association-field/AssociationFieldModeProvider.tsx create mode 100644 packages/core/client/src/schema-templates/BlockTemplateProvider.tsx create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/ActionDrawer.style.ts create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/ActionDrawer.tsx create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/BasicZIndexProvider.tsx create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/FilterAction.tsx create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/InternalPopoverNester.style.ts create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/InternalPopoverNester.tsx create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/ResetSchemaOptionsProvider.tsx rename packages/plugins/@nocobase/plugin-mobile/src/client/{pages => adaptor-of-desktop}/mobile-action-page/MobileActionPage.style.ts (68%) create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.tsx rename packages/plugins/@nocobase/plugin-mobile/src/client/{pages => adaptor-of-desktop}/mobile-action-page/MobileTabsForMobileActionPage.style.ts (100%) rename packages/plugins/@nocobase/plugin-mobile/src/client/{pages => adaptor-of-desktop}/mobile-action-page/MobileTabsForMobileActionPage.tsx (77%) create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/useGridCardBlockDecoratorProps.tsx delete mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/pages/mobile-action-page/MobileActionPage.tsx create mode 100644 packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowManualProvider.tsx diff --git a/lerna.json b/lerna.json index 95e098d1f6..7001a85e01 100644 --- a/lerna.json +++ b/lerna.json @@ -2,9 +2,7 @@ "version": "1.3.0-alpha", "npmClient": "yarn", "useWorkspaces": true, - "npmClientArgs": [ - "--ignore-engines" - ], + "npmClientArgs": ["--ignore-engines"], "command": { "version": { "forcePublish": true, diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index df84a3c970..9638469d64 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -20,7 +20,7 @@ import omit from 'lodash/omit'; import qs from 'qs'; import { ChangeEvent, useCallback, useContext, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { NavigateFunction } from 'react-router-dom'; +import { NavigateFunction, useHref } from 'react-router-dom'; import { useReactToPrint } from 'react-to-print'; import { AssociationFilter, @@ -1592,6 +1592,9 @@ export function useLinkActionProps(componentProps?: any) { const openInNewWindow = fieldSchema?.['x-component-props']?.['openInNewWindow']; const { parseURLAndParams } = useParseURLAndParams(); + // see: https://stackoverflow.com/questions/50449423/accessing-basename-of-browserouter + const basenameOfCurrentRouter = useHref('/'); + return { type: 'default', async onClick() { @@ -1605,7 +1608,7 @@ export function useLinkActionProps(componentProps?: any) { if (openInNewWindow) { window.open(completeURL(link), '_blank'); } else { - navigateWithinSelf(link, navigate); + navigateWithinSelf(link, navigate, window.location.origin + basenameOfCurrentRouter); } } else { console.error('link should be a string'); @@ -1719,18 +1722,18 @@ export function completeURL(url: string, origin = window.location.origin) { return url.startsWith('/') ? `${origin}${url}` : `${origin}/${url}`; } -export function navigateWithinSelf(link: string, navigate: NavigateFunction, origin = window.location.origin) { +export function navigateWithinSelf(link: string, navigate: NavigateFunction, basePath = window.location.origin) { if (!_.isString(link)) { return console.error('link should be a string'); } if (isURL(link)) { - if (link.startsWith(origin)) { - navigate(link.replace(origin, '')); + if (link.startsWith(basePath)) { + navigate(completeURL(link.replace(basePath, ''), '')); } else { window.open(link, '_self'); } } else { - navigate(link.startsWith('/') ? link : `/${link}`); + navigate(completeURL(link, '')); } } diff --git a/packages/core/client/src/block-provider/hooks/useBlockHeightProps.tsx b/packages/core/client/src/block-provider/hooks/useBlockHeightProps.tsx index 7feba143e2..fedee68b8e 100644 --- a/packages/core/client/src/block-provider/hooks/useBlockHeightProps.tsx +++ b/packages/core/client/src/block-provider/hooks/useBlockHeightProps.tsx @@ -9,7 +9,7 @@ import { useFieldSchema } from '@formily/react'; import { useMemo } from 'react'; -import { useBlockTemplateContext } from '../../schema-templates/BlockTemplate'; +import { useBlockTemplateContext } from '../../schema-templates/BlockTemplateProvider'; export const useBlockHeightProps = () => { const fieldSchema = useFieldSchema(); diff --git a/packages/core/client/src/global-theme/AntdAppProvider.tsx b/packages/core/client/src/global-theme/AntdAppProvider.tsx index 980f78621b..c4ead0ccbf 100644 --- a/packages/core/client/src/global-theme/AntdAppProvider.tsx +++ b/packages/core/client/src/global-theme/AntdAppProvider.tsx @@ -26,11 +26,21 @@ const AppInner = memo(({ children }: { children: React.ReactNode }) => { }); AppInner.displayName = 'AppInner'; -const AntdAppProvider = ({ children }: { children: React.ReactNode }) => { +const AntdAppProvider = ({ + children, + className, + style, +}: { + children: React.ReactNode; + className?: string; + style?: React.CSSProperties; +}) => { return ( {children} diff --git a/packages/core/client/src/modules/actions/add-new/createFormBlockInitializers.tsx b/packages/core/client/src/modules/actions/add-new/createFormBlockInitializers.tsx index 3089cf9b5b..b8a6a5e1bb 100644 --- a/packages/core/client/src/modules/actions/add-new/createFormBlockInitializers.tsx +++ b/packages/core/client/src/modules/actions/add-new/createFormBlockInitializers.tsx @@ -44,7 +44,7 @@ const commonOptions = { showAssociationFields: true, onlyCurrentDataSource: true, hideSearch: true, - componentType: 'FormItem', + componentType: `FormItem`, currentText: t('Current collection'), otherText: t('Other collections'), }, diff --git a/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx b/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx index 7bbf375e39..f6a3e1c72b 100644 --- a/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx +++ b/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx @@ -29,10 +29,11 @@ export const SchemaSettingsActionLinkItem: FC const { dn } = useDesignable(); const { t } = useTranslation(); const { urlSchema, paramsSchema, openInNewWindowSchema } = useURLAndHTMLSchema(); + const componentProps = fieldSchema['x-component-props'] || {}; const initialValues = { - url: field.componentProps.url, - params: field.componentProps.params || [{}], - openInNewWindow: field.componentProps.openInNewWindow, + url: componentProps.url, + params: componentProps.params || [{}], + openInNewWindow: componentProps.openInNewWindow, }; return ( @@ -52,7 +53,6 @@ export const SchemaSettingsActionLinkItem: FC }, }} onSubmit={({ url, params, openInNewWindow }) => { - const componentProps = fieldSchema['x-component-props'] || {}; componentProps.url = url; componentProps.params = params; componentProps.openInNewWindow = openInNewWindow; diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/detailsWithPaginationSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/details-multi/detailsWithPaginationSettings.tsx index 8f2df9d13e..2bf5765b62 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/detailsWithPaginationSettings.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/detailsWithPaginationSettings.tsx @@ -21,6 +21,7 @@ import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/Schem import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate'; +import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider'; import { setDataLoadingModeSettingsItem } from './setDataLoadingModeSettingsItem'; const commonItems: SchemaSettingsItemType[] = [ @@ -201,10 +202,11 @@ const commonItems: SchemaSettingsItemType[] = [ useComponentProps() { const { name } = useCollection_deprecated(); const fieldSchema = useFieldSchema(); + const { componentNamePrefix } = useBlockTemplateContext(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { - componentName: 'Details', + componentName: `${componentNamePrefix}Details`, collectionName: name, resourceName: defaultResource, }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/detailsBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/detailsBlockSettings.ts index a7a6759493..44bd1a1628 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-single/detailsBlockSettings.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/detailsBlockSettings.ts @@ -10,10 +10,11 @@ import { useFieldSchema } from '@formily/react'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { SchemaSettingsItemType } from '../../../../application/schema-settings/types'; -import { useCollection_deprecated } from '../../../../collection-manager'; +import { useCollection } from '../../../../data-source/collection/CollectionProvider'; import { SchemaSettingsFormItemTemplate, SchemaSettingsLinkageRules } from '../../../../schema-settings'; -import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; +import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; +import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider'; const commonItems: SchemaSettingsItemType[] = [ { name: 'title', @@ -27,7 +28,7 @@ const commonItems: SchemaSettingsItemType[] = [ name: 'linkageRules', Component: SchemaSettingsLinkageRules, useComponentProps() { - const { name } = useCollection_deprecated(); + const { name } = useCollection(); return { collectionName: name, readPretty: true, @@ -38,13 +39,14 @@ const commonItems: SchemaSettingsItemType[] = [ name: 'formItemTemplate', Component: SchemaSettingsFormItemTemplate, useComponentProps() { - const { name } = useCollection_deprecated(); + const { name } = useCollection(); const fieldSchema = useFieldSchema(); + const { componentNamePrefix } = useBlockTemplateContext(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { insertAdjacentPosition: 'beforeEnd', - componentName: 'ReadPrettyFormItem', + componentName: `${componentNamePrefix}ReadPrettyFormItem`, collectionName: name, resourceName: defaultResource, }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/createFormBlockSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/createFormBlockSettings.tsx index d50e557cc6..b806db3637 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/createFormBlockSettings.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/form/createFormBlockSettings.tsx @@ -11,6 +11,7 @@ import { useFieldSchema } from '@formily/react'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider'; import { useCollection_deprecated } from '../../../../collection-manager'; +import { useCollection } from '../../../../data-source/collection/CollectionProvider'; import { SchemaSettingsDataTemplates, SchemaSettingsFormItemTemplate, @@ -18,6 +19,7 @@ import { } from '../../../../schema-settings'; import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; +import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider'; export const createFormBlockSettings = new SchemaSettings({ name: 'blockSettings:createForm', @@ -62,12 +64,13 @@ export const createFormBlockSettings = new SchemaSettings({ name: 'formItemTemplate', Component: SchemaSettingsFormItemTemplate, useComponentProps() { - const { name } = useCollection_deprecated(); + const { componentNamePrefix } = useBlockTemplateContext(); + const { name } = useCollection(); const fieldSchema = useFieldSchema(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { - componentName: 'FormItem', + componentName: `${componentNamePrefix}FormItem`, collectionName: name, resourceName: defaultResource, }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/editFormBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/form/editFormBlockSettings.ts index 60cfc07ef6..7b065a877d 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/editFormBlockSettings.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/editFormBlockSettings.ts @@ -11,6 +11,7 @@ import { useFieldSchema } from '@formily/react'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider'; import { useCollection_deprecated } from '../../../../collection-manager'; +import { useCollection } from '../../../../data-source/collection/CollectionProvider'; import { SchemaSettingsDataTemplates, SchemaSettingsFormItemTemplate, @@ -18,6 +19,7 @@ import { } from '../../../../schema-settings'; import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; +import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider'; export const editFormBlockSettings = new SchemaSettings({ name: 'blockSettings:editForm', @@ -62,12 +64,13 @@ export const editFormBlockSettings = new SchemaSettings({ name: 'formItemTemplate', Component: SchemaSettingsFormItemTemplate, useComponentProps() { - const { name } = useCollection_deprecated(); + const { componentNamePrefix } = useBlockTemplateContext(); + const { name } = useCollection(); const fieldSchema = useFieldSchema(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { - componentName: 'FormItem', + componentName: `${componentNamePrefix}FormItem`, collectionName: name, resourceName: defaultResource, }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardBlockInitializer.tsx index f38057bc5d..6be498c8f8 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardBlockInitializer.tsx @@ -49,7 +49,7 @@ export const GridCardBlockInitializer = ({ } - componentType={'GridCard'} + componentType={`GridCard`} onCreateBlockSchema={async (options) => { if (createBlockSchema) { return createBlockSchema(options); diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts index 885297e018..f9e87c9221 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts @@ -20,9 +20,9 @@ import { pageSizeOptions } from '../../../../schema-component/antd/grid-card/opt import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate'; +import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider'; import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem'; import { SetTheCountOfColumnsDisplayedInARow } from './SetTheCountOfColumnsDisplayedInARow'; -import { useCollection } from '../../../../data-source'; export const gridCardBlockSettings = new SchemaSettings({ name: 'blockSettings:gridCard', @@ -210,11 +210,12 @@ export const gridCardBlockSettings = new SchemaSettings({ useComponentProps() { const { name } = useCollection_deprecated(); const fieldSchema = useFieldSchema(); + const { componentNamePrefix } = useBlockTemplateContext(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { - componentName: 'GridCard', + componentName: `${componentNamePrefix}GridCard`, collectionName: name, resourceName: defaultResource, }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/ListBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/list/ListBlockInitializer.tsx index 4e9e48dd22..a1fea24887 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/list/ListBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/list/ListBlockInitializer.tsx @@ -49,7 +49,7 @@ export const ListBlockInitializer = ({ } - componentType={'List'} + componentType={`List`} onCreateBlockSchema={async (options) => { if (createBlockSchema) { return createBlockSchema(options); diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts index 5d17d78e1d..9a572040c3 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts @@ -19,8 +19,8 @@ import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/Schem import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate'; +import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider'; import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem'; -import { useCollection } from '../../../../data-source'; export const listBlockSettings = new SchemaSettings({ name: 'blockSettings:list', @@ -212,11 +212,12 @@ export const listBlockSettings = new SchemaSettings({ useComponentProps() { const { name } = useCollection_deprecated(); const fieldSchema = useFieldSchema(); + const { componentNamePrefix } = useBlockTemplateContext(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { - componentName: 'List', + componentName: `${componentNamePrefix}List`, collectionName: name, resourceName: defaultResource, }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx index 77291a12da..bc3695f6df 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx @@ -44,7 +44,7 @@ export const TableBlockInitializer = ({ } - componentType={'Table'} + componentType={`Table`} onCreateBlockSchema={async (options) => { if (createBlockSchema) { return createBlockSchema(options); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx index f8404cd9fa..f7d70ff2b1 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx @@ -23,8 +23,8 @@ import { SchemaSettingsSortField } from '../../../../schema-settings/SchemaSetti import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate'; import { setDefaultSortingRulesSchemaSettingsItem } from '../../../../schema-settings/setDefaultSortingRulesSchemaSettingsItem'; import { setTheDataScopeSchemaSettingsItem } from '../../../../schema-settings/setTheDataScopeSchemaSettingsItem'; +import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider'; import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem'; -import { useCollection } from '../../../../data-source'; export const tableBlockSettings = new SchemaSettings({ name: 'blockSettings:table', @@ -212,10 +212,11 @@ export const tableBlockSettings = new SchemaSettings({ useComponentProps() { const { name } = useCollection_deprecated(); const fieldSchema = useFieldSchema(); + const { componentNamePrefix } = useBlockTemplateContext(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { - componentName: 'Table', + componentName: `${componentNamePrefix}Table`, collectionName: name, resourceName: defaultResource, }; diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/FilterCollapseBlockInitializer.tsx b/packages/core/client/src/modules/blocks/filter-blocks/collapse/FilterCollapseBlockInitializer.tsx index 1691478bcf..d01881388d 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/collapse/FilterCollapseBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/FilterCollapseBlockInitializer.tsx @@ -11,9 +11,9 @@ import { TableOutlined } from '@ant-design/icons'; import React from 'react'; import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; -import { createCollapseBlockSchema } from './createFilterCollapseBlockSchema'; -import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; import { Collection, CollectionFieldOptions } from '../../../../data-source'; +import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; +import { createCollapseBlockSchema } from './createFilterCollapseBlockSchema'; export const FilterCollapseBlockInitializer = ({ filterCollections, @@ -32,7 +32,7 @@ export const FilterCollapseBlockInitializer = ({ {...itemConfig} onlyCurrentDataSource={onlyCurrentDataSource} icon={} - componentType={'FilterCollapse'} + componentType={`FilterCollapse`} onCreateBlockSchema={async ({ item }) => { const schema = createCollapseBlockSchema({ dataSource: item.dataSource, diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseBlockSettings.ts b/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseBlockSettings.ts index ee263d4cf4..503c1c6ada 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseBlockSettings.ts +++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseBlockSettings.ts @@ -12,10 +12,11 @@ import { useTranslation } from 'react-i18next'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { useCollection_deprecated } from '../../../../collection-manager'; import { FilterBlockType } from '../../../../filter-provider'; +import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks'; import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate'; -import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; +import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider'; export const filterCollapseBlockSettings = new SchemaSettings({ name: 'blockSettings:filterCollapse', @@ -34,11 +35,12 @@ export const filterCollapseBlockSettings = new SchemaSettings({ useComponentProps() { const { name } = useCollection_deprecated(); const fieldSchema = useFieldSchema(); + const { componentNamePrefix } = useBlockTemplateContext(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { - componentName: 'FilterCollapse', + componentName: `${componentNamePrefix}FilterCollapse`, collectionName: name, resourceName: defaultResource, }; diff --git a/packages/core/client/src/modules/blocks/filter-blocks/form/FilterFormBlockInitializer.tsx b/packages/core/client/src/modules/blocks/filter-blocks/form/FilterFormBlockInitializer.tsx index ed67c1ffbd..d966b8dc89 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/form/FilterFormBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/filter-blocks/form/FilterFormBlockInitializer.tsx @@ -10,9 +10,10 @@ import { FormOutlined } from '@ant-design/icons'; import React from 'react'; import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; -import { createFilterFormBlockSchema } from './createFilterFormBlockSchema'; -import { FilterBlockInitializer } from '../../../../schema-initializer/items/FilterBlockInitializer'; import { Collection, CollectionFieldOptions } from '../../../../data-source'; +import { FilterBlockInitializer } from '../../../../schema-initializer/items/FilterBlockInitializer'; +import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider'; +import { createFilterFormBlockSchema } from './createFilterFormBlockSchema'; export const FilterFormBlockInitializer = ({ filterCollections, @@ -25,13 +26,14 @@ export const FilterFormBlockInitializer = ({ }) => { const itemConfig = useSchemaInitializerItem(); const { insert } = useSchemaInitializer(); + const { componentNamePrefix } = useBlockTemplateContext(); return ( } onlyCurrentDataSource={onlyCurrentDataSource} - componentType={'FilterFormItem'} + componentType={`${componentNamePrefix}FilterFormItem`} templateWrap={(templateSchema, { item }) => { const s = createFilterFormBlockSchema({ templateSchema: templateSchema, diff --git a/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormBlockSettings.ts b/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormBlockSettings.ts index 9b33418bd0..b0bf10d376 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormBlockSettings.ts +++ b/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormBlockSettings.ts @@ -11,11 +11,13 @@ import { useFieldSchema } from '@formily/react'; import { useTranslation } from 'react-i18next'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { useCollection_deprecated } from '../../../../collection-manager'; +import { useCollection } from '../../../../data-source/collection/CollectionProvider'; import { FilterBlockType } from '../../../../filter-provider'; import { SchemaSettingsFormItemTemplate, SchemaSettingsLinkageRules } from '../../../../schema-settings'; +import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks'; -import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; +import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider'; export const filterFormBlockSettings = new SchemaSettings({ name: 'blockSettings:filterForm', @@ -32,12 +34,13 @@ export const filterFormBlockSettings = new SchemaSettings({ name: 'formItemTemplate', Component: SchemaSettingsFormItemTemplate, useComponentProps() { - const { name } = useCollection_deprecated(); + const { componentNamePrefix } = useBlockTemplateContext(); + const { name } = useCollection(); const fieldSchema = useFieldSchema(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { - componentName: 'FilterFormItem', + componentName: `${componentNamePrefix}FilterFormItem`, collectionName: name, resourceName: defaultResource, }; diff --git a/packages/core/client/src/schema-component/antd/action/Action.Container.tsx b/packages/core/client/src/schema-component/antd/action/Action.Container.tsx index 8e4f48f83a..d5525110cd 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Container.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Container.tsx @@ -16,8 +16,8 @@ import { ComposedActionDrawer } from './types'; export const ActionContainer: ComposedActionDrawer = observer( (props: any) => { - const { openMode } = useActionContext(); - const { getComponentByOpenMode } = useOpenModeContext(); + const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext(); + const { openMode = defaultOpenMode } = useActionContext(); const { currentLevel } = useCurrentPopupContext(); const Component = getComponentByOpenMode(openMode); diff --git a/packages/core/client/src/schema-component/antd/action/context.tsx b/packages/core/client/src/schema-component/antd/action/context.tsx index e7eecf9c62..951c35fed9 100644 --- a/packages/core/client/src/schema-component/antd/action/context.tsx +++ b/packages/core/client/src/schema-component/antd/action/context.tsx @@ -43,19 +43,19 @@ export const ActionContextProvider: React.FC { const { params } = useCurrentPopupContext(); + const fieldSchema = useFieldSchema(); const popupUidWithoutOpened = useFieldSchema()?.['x-uid']; const service = useDataBlockRequest(); const currentPopupUid = params?.popupuid; - // By using caching, we solve the problem of not being able to obtain the correct service when closing a popup through a URL + // 把 service 存起来 useEffect(() => { - // This case refers to when the current button is rendered on a page or in a popup if (popupUidWithoutOpened && currentPopupUid !== popupUidWithoutOpened) { storeBlockService(popupUidWithoutOpened, { service }); } - }, [popupUidWithoutOpened, service, currentPopupUid]); + }, [popupUidWithoutOpened, service, currentPopupUid, fieldSchema]); - // This case refers to when the current button is closed as a popup (the button's uid is the same as the popup's uid) + // 关闭弹窗时,获取到对应的 service if (currentPopupUid === popupUidWithoutOpened) { return getBlockService(currentPopupUid)?.service || service; } diff --git a/packages/core/client/src/schema-component/antd/action/demos/demo1.tsx b/packages/core/client/src/schema-component/antd/action/demos/demo1.tsx index e09d88fcd7..fd140cddf2 100644 --- a/packages/core/client/src/schema-component/antd/action/demos/demo1.tsx +++ b/packages/core/client/src/schema-component/antd/action/demos/demo1.tsx @@ -1,8 +1,7 @@ - - import { ISchema, observer, useForm } from '@formily/react'; import { Action, + CustomRouterContextProvider, Form, FormItem, Input, @@ -11,6 +10,7 @@ import { useActionContext, } from '@nocobase/client'; import React from 'react'; +import { Router } from 'react-router-dom'; const useCloseAction = () => { const { setVisible } = useActionContext(); @@ -67,8 +67,12 @@ const schema: ISchema = { export default observer(() => { return ( - - - + + + + + + + ); }); diff --git a/packages/core/client/src/schema-component/antd/action/demos/demo2.tsx b/packages/core/client/src/schema-component/antd/action/demos/demo2.tsx index caea02d0b5..1388975ad8 100644 --- a/packages/core/client/src/schema-component/antd/action/demos/demo2.tsx +++ b/packages/core/client/src/schema-component/antd/action/demos/demo2.tsx @@ -1,9 +1,8 @@ - - import { ISchema, observer, useForm } from '@formily/react'; import { Action, ActionContextProvider, + CustomRouterContextProvider, Form, FormItem, Input, @@ -12,6 +11,7 @@ import { useActionContext, } from '@nocobase/client'; import React, { useState } from 'react'; +import { Router } from 'react-router-dom'; const useCloseAction = () => { const { setVisible } = useActionContext(); @@ -59,11 +59,15 @@ const schema: ISchema = { export default observer(() => { const [visible, setVisible] = useState(false); return ( - - - setVisible(true)}>Open - - - + + + + + setVisible(true)}>Open + + + + + ); }); diff --git a/packages/core/client/src/schema-component/antd/action/demos/demo4.tsx b/packages/core/client/src/schema-component/antd/action/demos/demo4.tsx index ea898b565c..75d797c7b9 100644 --- a/packages/core/client/src/schema-component/antd/action/demos/demo4.tsx +++ b/packages/core/client/src/schema-component/antd/action/demos/demo4.tsx @@ -1,8 +1,7 @@ - - import { ISchema, observer, useForm } from '@formily/react'; import { Action, + CustomRouterContextProvider, Form, FormItem, Input, @@ -11,6 +10,7 @@ import { useActionContext, } from '@nocobase/client'; import React from 'react'; +import { Router } from 'react-router-dom'; const useCloseAction = () => { const { setVisible } = useActionContext(); @@ -55,8 +55,12 @@ const schema: ISchema = { export default observer(() => { return ( - - - + + + + + + + ); }); diff --git a/packages/core/client/src/schema-component/antd/association-field/AssociationFieldModeProvider.tsx b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldModeProvider.tsx new file mode 100644 index 0000000000..59cae720f0 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldModeProvider.tsx @@ -0,0 +1,81 @@ +/** + * 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 _ from 'lodash'; +import React, { createContext, useCallback, useMemo } from 'react'; +import { AssociationSelect } from './AssociationSelect'; +import { InternalFileManager } from './FileManager'; +import { InternalCascadeSelect } from './InternalCascadeSelect'; +import { InternalNester } from './InternalNester'; +import { InternalPicker } from './InternalPicker'; +import { InternalPopoverNester } from './InternalPopoverNester'; +import { InternalSubTable } from './InternalSubTable'; + +export enum AssociationFieldMode { + Picker = 'Picker', + Nester = 'Nester', + PopoverNester = 'PopoverNester', + Select = 'Select', + SubTable = 'SubTable', + FileManager = 'FileManager', + CascadeSelect = 'CascadeSelect', + Tag = 'Tag', +} + +interface AssociationFieldModeProviderProps { + modeToComponent: Partial React.FC)>>; +} + +const defaultModeToComponent = { + Picker: InternalPicker, + Nester: InternalNester, + PopoverNester: InternalPopoverNester, + Select: AssociationSelect, + SubTable: InternalSubTable, + FileManager: InternalFileManager, + CascadeSelect: InternalCascadeSelect, +}; + +const AssociationFieldModeContext = createContext<{ + modeToComponent: AssociationFieldModeProviderProps['modeToComponent']; + getComponent: (mode: AssociationFieldMode) => React.FC; +}>({ + modeToComponent: defaultModeToComponent, + getComponent: (mode: AssociationFieldMode) => { + return defaultModeToComponent[mode]; + }, +}); + +export const AssociationFieldModeProvider: React.FC = (props) => { + const modeContext = useAssociationFieldModeContext(); + const modeToComponent = useMemo( + () => ({ ...modeContext.modeToComponent, ...props.modeToComponent }), + [modeContext.modeToComponent, props.modeToComponent], + ); + + const getComponent = useCallback( + (mode: AssociationFieldMode) => { + const component = modeToComponent[mode]; + + if (_.isFunction(component)) { + return component(defaultModeToComponent[mode]) as React.FC; + } + + return component || defaultModeToComponent[mode]; + }, + [modeToComponent], + ); + + const value = useMemo(() => ({ modeToComponent, getComponent }), [getComponent, modeToComponent]); + return {props.children}; +}; + +export const useAssociationFieldModeContext = () => { + return React.useContext(AssociationFieldModeContext); +}; diff --git a/packages/core/client/src/schema-component/antd/association-field/Editable.tsx b/packages/core/client/src/schema-component/antd/association-field/Editable.tsx index e032636e2d..4b42a17c0b 100644 --- a/packages/core/client/src/schema-component/antd/association-field/Editable.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/Editable.tsx @@ -13,14 +13,8 @@ import React from 'react'; import { SchemaComponentOptions } from '../../'; import { useAssociationCreateActionProps as useCAP } from '../../../block-provider/hooks'; import { useCollection_deprecated } from '../../../collection-manager'; +import { useAssociationFieldModeContext } from './AssociationFieldModeProvider'; import { AssociationFieldProvider } from './AssociationFieldProvider'; -import { AssociationSelect } from './AssociationSelect'; -import { InternalFileManager } from './FileManager'; -import { InternalCascadeSelect } from './InternalCascadeSelect'; -import { InternalNester } from './InternalNester'; -import { InternalPicker } from './InternalPicker'; -import { InternalPopoverNester } from './InternalPopoverNester'; -import { InternalSubTable } from './InternalSubTable'; import { CreateRecordAction } from './components/CreateRecordAction'; import { useAssociationFieldContext } from './hooks'; @@ -30,6 +24,7 @@ const EditableAssociationField = observer( const field: Field = useField(); const form = useForm(); const { options: collectionField, currentMode } = useAssociationFieldContext(); + const { getComponent } = useAssociationFieldModeContext(); const useCreateActionProps = () => { const { onClick } = useCAP(); @@ -57,15 +52,11 @@ const EditableAssociationField = observer( }; }; + const Component = getComponent(currentMode); + return ( - {currentMode === 'Picker' && } - {currentMode === 'Nester' && } - {currentMode === 'PopoverNester' && } - {currentMode === 'Select' && } - {currentMode === 'SubTable' && } - {currentMode === 'FileManager' && } - {currentMode === 'CascadeSelect' && } + ); }, diff --git a/packages/core/client/src/schema-component/antd/association-field/InternalPopoverNester.tsx b/packages/core/client/src/schema-component/antd/association-field/InternalPopoverNester.tsx index 3dc365a5b1..025dbae8ba 100644 --- a/packages/core/client/src/schema-component/antd/association-field/InternalPopoverNester.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/InternalPopoverNester.tsx @@ -10,7 +10,7 @@ import { EditOutlined } from '@ant-design/icons'; import { css } from '@emotion/css'; import { observer, useFieldSchema } from '@formily/react'; -import React, { useContext, useRef, useState } from 'react'; +import React, { useCallback, useContext, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ActionContext, ActionContextProvider } from '../action/context'; import { useGetAriaLabelOfPopover } from '../action/hooks/useGetAriaLabelOfPopover'; @@ -22,6 +22,7 @@ import { useAssociationFieldContext } from './hooks'; const InternalPopoverNesterContentCss = css` min-width: 600px; + max-width: 800px; max-height: 440px; overflow: auto; .ant-card { @@ -30,7 +31,16 @@ const InternalPopoverNesterContentCss = css` `; export const InternalPopoverNester = observer( - (props) => { + (props: { + Container?: (props: { + open: boolean; + onOpenChange: (open: boolean) => void; + trigger: 'click' | 'hover'; + content: React.ReactElement; + children: React.ReactElement; + }) => React.ReactElement; + children?: React.ReactElement; + }) => { const { options } = useAssociationFieldContext(); const [visible, setVisible] = useState(false); const { t } = useTranslation(); @@ -42,11 +52,7 @@ export const InternalPopoverNester = observer( shouldMountElement: true, }; const content = ( -
+
); @@ -56,6 +62,11 @@ export const InternalPopoverNester = observer( getContainer: getContainer, }; const { getAriaLabel } = useGetAriaLabelOfPopover(); + const Container = props.Container || StablePopover; + const handleOpenChange = useCallback((open: boolean) => { + setVisible(open); + }, []); + const overlayStyle = useMemo(() => ({ padding: '0px' }), []); if (process.env.__E2E__) { useSetAriaLabelForPopover(visible); @@ -63,13 +74,13 @@ export const InternalPopoverNester = observer( return ( - setVisible(open)} + onOpenChange={handleOpenChange} title={t(options?.uiSchema?.rawTitle)} > @@ -82,7 +93,7 @@ export const InternalPopoverNester = observer(
- + {visible && (
{ @@ -51,7 +52,7 @@ export function useAssociationFieldContext() { return useContext(AssociationFieldContext) as { options: any; field: F; - currentMode: string; + currentMode: AssociationFieldMode; allowMultiple?: boolean; allowDissociate?: boolean; }; diff --git a/packages/core/client/src/schema-component/antd/association-field/index.ts b/packages/core/client/src/schema-component/antd/association-field/index.ts index f3e93bb311..427391c679 100644 --- a/packages/core/client/src/schema-component/antd/association-field/index.ts +++ b/packages/core/client/src/schema-component/antd/association-field/index.ts @@ -15,6 +15,7 @@ import { Nester } from './Nester'; import { ReadPretty } from './ReadPretty'; import { SubTable } from './SubTable'; +export { AssociationFieldModeProvider } from './AssociationFieldModeProvider'; export const AssociationField: any = connect(Editable, mapReadPretty(ReadPretty)); AssociationField.SubTable = SubTable; diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.BlockDesigner.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.BlockDesigner.tsx index 95df01c582..99140fab69 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.BlockDesigner.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.BlockDesigner.tsx @@ -17,19 +17,25 @@ import { SchemaSettingsBlockTitleItem } from '../../../schema-settings/SchemaSet import { SchemaSettingsConnectDataBlocks } from '../../../schema-settings/SchemaSettingsConnectDataBlocks'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; import { useSchemaTemplate } from '../../../schema-templates'; +import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider'; export const AssociationFilterBlockDesigner = () => { const { name, title } = useCollection_deprecated(); const template = useSchemaTemplate(); const fieldSchema = useFieldSchema(); const { t } = useTranslation(); + const { componentNamePrefix } = useBlockTemplateContext(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return ( - + = ActionProps & { form?: Form; onSubmit?: (values: T) => void; onReset?: (values: T) => void; + /** + * @default Popover + * 在移动端中,会使用移动端的 Popup 组件 + */ + Container?: (props: { + open: boolean; + onOpenChange: (open: boolean) => void; + trigger: 'click' | 'hover'; + content: React.ReactElement; + children: React.ReactElement; + }) => React.ReactElement; }; export const FilterAction = withDynamicSchemaProps( @@ -42,7 +53,7 @@ export const FilterAction = withDynamicSchemaProps( const form = useMemo
(() => props.form || createForm(), []); // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema - const { options, onSubmit, onReset, ...others } = useProps(props); + const { options, onSubmit, onReset, Container = StablePopover, ...others } = useProps(props); const onOpenChange = useCallback((visible: boolean): void => { setVisible(visible); @@ -50,7 +61,7 @@ export const FilterAction = withDynamicSchemaProps( return ( - } > - - + setVisible(!visible)} {...others} title={field.title} /> + ); }), diff --git a/packages/core/client/src/schema-component/antd/filter/FilterItem.tsx b/packages/core/client/src/schema-component/antd/filter/FilterItem.tsx index 1bcf2dfc76..76572fb1ae 100644 --- a/packages/core/client/src/schema-component/antd/filter/FilterItem.tsx +++ b/packages/core/client/src/schema-component/antd/filter/FilterItem.tsx @@ -62,7 +62,7 @@ export const FilterItem = observer( return ( // 添加 nc-filter-item 类名是为了帮助编写测试时更容易选中该元素
- + - {!operator?.noValue ? ( - - ) : null} - {!props.disabled && ( - - - - )} + + {!operator?.noValue ? ( + + ) : null} + {!props.disabled && ( + + + + )} +
); diff --git a/packages/core/client/src/schema-component/antd/filter/demos/demo5.tsx b/packages/core/client/src/schema-component/antd/filter/demos/demo5.tsx index 1befe4c567..a97247aeca 100644 --- a/packages/core/client/src/schema-component/antd/filter/demos/demo5.tsx +++ b/packages/core/client/src/schema-component/antd/filter/demos/demo5.tsx @@ -1,8 +1,14 @@ - - import { ISchema } from '@formily/json-schema'; -import { Filter, FilterAction, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; +import { + CustomRouterContextProvider, + Filter, + FilterAction, + Input, + SchemaComponent, + SchemaComponentProvider, +} from '@nocobase/client'; import React from 'react'; +import { Router } from 'react-router-dom'; const options = [ { @@ -99,8 +105,12 @@ const schema: ISchema = { export default () => { return ( - - - + + + + + + + ); }; diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.Settings.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.Settings.tsx index ab67117ebe..76f6c84c85 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Form.Settings.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.Settings.tsx @@ -15,6 +15,7 @@ import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProv import { useFormBlockContext } from '../../../block-provider/FormBlockProvider'; import { useCollection_deprecated } from '../../../collection-manager'; import { useSortFields } from '../../../collection-manager/action-hooks'; +import { useCollection } from '../../../data-source/collection/CollectionProvider'; import { setDataLoadingModeSettingsItem } from '../../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem'; import { SchemaSettingsDataTemplates, @@ -25,6 +26,7 @@ import { SchemaSettingsBlockHeightItem } from '../../../schema-settings/SchemaSe import { SchemaSettingsBlockTitleItem } from '../../../schema-settings/SchemaSettingsBlockTitleItem'; import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; +import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider'; import { useDesignable } from '../../hooks'; import { removeNullCondition } from '../filter'; @@ -74,12 +76,13 @@ export const formSettings = new SchemaSettings({ name: 'formItemTemplate', Component: SchemaSettingsFormItemTemplate, useComponentProps() { - const { name } = useCollection_deprecated(); + const { componentNamePrefix } = useBlockTemplateContext(); + const { name } = useCollection(); const fieldSchema = useFieldSchema(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { - componentName: 'FormItem', + componentName: `${componentNamePrefix}FormItem`, collectionName: name, resourceName: defaultResource, }; @@ -127,13 +130,14 @@ export const readPrettyFormSettings = new SchemaSettings({ name: 'formItemTemplate', Component: SchemaSettingsFormItemTemplate, useComponentProps() { - const { name } = useCollection_deprecated(); + const { componentNamePrefix } = useBlockTemplateContext(); + const { name } = useCollection(); const fieldSchema = useFieldSchema(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { insertAdjacentPosition: 'beforeEnd', - componentName: 'ReadPrettyFormItem', + componentName: `${componentNamePrefix}ReadPrettyFormItem`, collectionName: name, resourceName: defaultResource, }; @@ -336,10 +340,11 @@ export const formDetailsSettings = new SchemaSettings({ useComponentProps() { const { name } = useCollection_deprecated(); const fieldSchema = useFieldSchema(); + const { componentNamePrefix } = useBlockTemplateContext(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { - componentName: 'Details', + componentName: `${componentNamePrefix}Details`, collectionName: name, resourceName: defaultResource, }; diff --git a/packages/core/client/src/schema-component/antd/form/Form.Settings.tsx b/packages/core/client/src/schema-component/antd/form/Form.Settings.tsx index 6ac7cfedfd..2c743c1773 100644 --- a/packages/core/client/src/schema-component/antd/form/Form.Settings.tsx +++ b/packages/core/client/src/schema-component/antd/form/Form.Settings.tsx @@ -10,6 +10,7 @@ import { SchemaSettings } from '../../../application/schema-settings'; import { useCollection_deprecated } from '../../../collection-manager'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; +import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider'; /** * @deprecated @@ -22,8 +23,9 @@ export const formV1Settings = new SchemaSettings({ Component: SchemaSettingsTemplate, useComponentProps() { const { name } = useCollection_deprecated(); + const { componentNamePrefix } = useBlockTemplateContext(); return { - componentName: 'Form', + componentName: `${componentNamePrefix}Form`, collectionName: name, }; }, diff --git a/packages/core/client/src/schema-component/antd/form/Form.tsx b/packages/core/client/src/schema-component/antd/form/Form.tsx index f3d339f402..4408f715c6 100644 --- a/packages/core/client/src/schema-component/antd/form/Form.tsx +++ b/packages/core/client/src/schema-component/antd/form/Form.tsx @@ -19,6 +19,7 @@ import { useCollection_deprecated } from '../../../collection-manager'; import { GeneralSchemaDesigner, SchemaSettingsDivider, SchemaSettingsRemove } from '../../../schema-settings'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; import { useSchemaTemplate } from '../../../schema-templates'; +import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider'; type Opts = Options & { uid?: string }; @@ -130,9 +131,10 @@ export const Form: React.FC & { Designer?: any } = observer( Form.Designer = function Designer() { const { name, title } = useCollection_deprecated(); const template = useSchemaTemplate(); + const { componentNamePrefix } = useBlockTemplateContext(); return ( - + { return ( - - - + + + + + + + ); }); diff --git a/packages/core/client/src/schema-component/antd/form/demos/demo2.tsx b/packages/core/client/src/schema-component/antd/form/demos/demo2.tsx index a5f3835219..04b6836103 100644 --- a/packages/core/client/src/schema-component/antd/form/demos/demo2.tsx +++ b/packages/core/client/src/schema-component/antd/form/demos/demo2.tsx @@ -1,10 +1,9 @@ - - import { FormItem, Input } from '@formily/antd-v5'; import { ISchema, observer, useForm } from '@formily/react'; -import { Action, Form, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; +import { Action, CustomRouterContextProvider, Form, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; import { Card } from 'antd'; import React from 'react'; +import { Router } from 'react-router-dom'; const schema: ISchema = { type: 'object', @@ -59,8 +58,12 @@ export default observer(() => { }; return ( - - - + + + + + + + ); }); diff --git a/packages/core/client/src/schema-component/antd/form/demos/demo3.tsx b/packages/core/client/src/schema-component/antd/form/demos/demo3.tsx index 15dd8e6f8e..28a63719f1 100644 --- a/packages/core/client/src/schema-component/antd/form/demos/demo3.tsx +++ b/packages/core/client/src/schema-component/antd/form/demos/demo3.tsx @@ -1,10 +1,9 @@ - - import { FormItem, Input } from '@formily/antd-v5'; import { ISchema, observer, useForm } from '@formily/react'; -import { Action, Form, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; +import { Action, CustomRouterContextProvider, Form, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; import { Card } from 'antd'; import React from 'react'; +import { Router } from 'react-router-dom'; const schema: ISchema = { type: 'object', @@ -58,8 +57,12 @@ export default observer(() => { }; return ( - - - + + + + + + + ); }); diff --git a/packages/core/client/src/schema-component/antd/form/demos/demo4.tsx b/packages/core/client/src/schema-component/antd/form/demos/demo4.tsx index a006fd9e5b..403ece0146 100644 --- a/packages/core/client/src/schema-component/antd/form/demos/demo4.tsx +++ b/packages/core/client/src/schema-component/antd/form/demos/demo4.tsx @@ -1,9 +1,15 @@ - - import { FormItem, Input } from '@formily/antd-v5'; import { ISchema, observer, useForm } from '@formily/react'; -import { Action, Form, SchemaComponent, SchemaComponentProvider, useCloseAction } from '@nocobase/client'; +import { + Action, + CustomRouterContextProvider, + Form, + SchemaComponent, + SchemaComponentProvider, + useCloseAction, +} from '@nocobase/client'; import React from 'react'; +import { Router } from 'react-router-dom'; const schema: ISchema = { type: 'object', @@ -65,8 +71,12 @@ export default observer(() => { ); return ( - - - + + + + + + + ); }); diff --git a/packages/core/client/src/schema-component/antd/form/demos/demo5.tsx b/packages/core/client/src/schema-component/antd/form/demos/demo5.tsx index b9de1817f6..ead158d2c0 100644 --- a/packages/core/client/src/schema-component/antd/form/demos/demo5.tsx +++ b/packages/core/client/src/schema-component/antd/form/demos/demo5.tsx @@ -1,10 +1,9 @@ - - import { FormItem, Input } from '@formily/antd-v5'; import { ISchema, observer, useForm } from '@formily/react'; import { APIClientProvider, Action, + CustomRouterContextProvider, Form, SchemaComponent, SchemaComponentProvider, @@ -12,6 +11,7 @@ import { } from '@nocobase/client'; import { Card, Space } from 'antd'; import React from 'react'; +import { Router } from 'react-router-dom'; import { apiClient } from './apiClient'; const schema: ISchema = { @@ -105,13 +105,17 @@ const useRefresh = () => { export default observer(() => { return ( - - - - - + + + + + + + + + ); }); diff --git a/packages/core/client/src/schema-component/antd/form/demos/demo6.tsx b/packages/core/client/src/schema-component/antd/form/demos/demo6.tsx index 032345e4e2..dc26a50d5e 100644 --- a/packages/core/client/src/schema-component/antd/form/demos/demo6.tsx +++ b/packages/core/client/src/schema-component/antd/form/demos/demo6.tsx @@ -1,10 +1,9 @@ - - import { FormItem, Input } from '@formily/antd-v5'; import { ISchema, observer, useForm } from '@formily/react'; -import { Action, Form, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; +import { Action, CustomRouterContextProvider, Form, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; import { Card } from 'antd'; import React from 'react'; +import { Router } from 'react-router-dom'; const schema: ISchema = { type: 'object', @@ -59,8 +58,12 @@ const useSubmit = () => { export default observer(() => { return ( - - - + + + + + + + ); }); diff --git a/packages/core/client/src/schema-component/antd/form/demos/demo7.tsx b/packages/core/client/src/schema-component/antd/form/demos/demo7.tsx index af3e930cd0..120abf8bfe 100644 --- a/packages/core/client/src/schema-component/antd/form/demos/demo7.tsx +++ b/packages/core/client/src/schema-component/antd/form/demos/demo7.tsx @@ -1,10 +1,17 @@ - - import { FormItem, Input } from '@formily/antd-v5'; import { ISchema, observer, useForm } from '@formily/react'; -import { Action, Form, FormUseValues, SchemaComponent, SchemaComponentProvider, useRequest } from '@nocobase/client'; +import { + Action, + CustomRouterContextProvider, + Form, + FormUseValues, + SchemaComponent, + SchemaComponentProvider, + useRequest, +} from '@nocobase/client'; import { Card } from 'antd'; import React from 'react'; +import { Router } from 'react-router-dom'; const schema: ISchema = { type: 'object', @@ -63,11 +70,15 @@ const useValues: FormUseValues = (opts) => { export default observer(() => { return ( - - - + + + + + + + ); }); diff --git a/packages/core/client/src/schema-component/antd/form/demos/demo8.tsx b/packages/core/client/src/schema-component/antd/form/demos/demo8.tsx index fbf1591a18..687a202c7c 100644 --- a/packages/core/client/src/schema-component/antd/form/demos/demo8.tsx +++ b/packages/core/client/src/schema-component/antd/form/demos/demo8.tsx @@ -1,10 +1,9 @@ - - import { FormItem } from '@formily/antd-v5'; import { ISchema, observer } from '@formily/react'; import { Action, ActionContextProvider, + CustomRouterContextProvider, Form, Input, SchemaComponent, @@ -15,6 +14,7 @@ import { } from '@nocobase/client'; import { Button } from 'antd'; import React, { useEffect, useState } from 'react'; +import { Router } from 'react-router-dom'; const useValues = (options) => { const { visible } = useActionContext(); @@ -72,17 +72,21 @@ export default observer(() => { const [visible, setVisible] = useState(false); return ( - - - - - - + + + + + + + + + + ); }); diff --git a/packages/core/client/src/schema-component/antd/grid-card/GridCard.Designer.tsx b/packages/core/client/src/schema-component/antd/grid-card/GridCard.Designer.tsx index 27d78a3765..aeff4cb361 100644 --- a/packages/core/client/src/schema-component/antd/grid-card/GridCard.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/grid-card/GridCard.Designer.tsx @@ -17,7 +17,6 @@ import { useFormBlockContext } from '../../../block-provider/FormBlockProvider'; import { useCollection_deprecated, useSortFields } from '../../../collection-manager'; import { SetDataLoadingMode } from '../../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem'; import { SetTheCountOfColumnsDisplayedInARow } from '../../../modules/blocks/data-blocks/grid-card/SetTheCountOfColumnsDisplayedInARow'; -import { useRecord } from '../../../record-provider'; import { GeneralSchemaDesigner, SchemaSettingsDivider, @@ -28,10 +27,11 @@ import { import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; import { useSchemaTemplate } from '../../../schema-templates'; +import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider'; import { SchemaComponentOptions } from '../../core'; import { useDesignable } from '../../hooks'; import { removeNullCondition } from '../filter'; -import { defaultColumnCount, gridSizes, pageSizeOptions, screenSizeMaps, screenSizeTitleMaps } from './options'; +import { gridSizes, pageSizeOptions, screenSizeMaps, screenSizeTitleMaps } from './options'; export const columnCountMarks = [1, 2, 3, 4, 6, 8, 12, 24].reduce((obj, cur) => { obj[cur] = cur; @@ -47,11 +47,10 @@ export const GridCardDesigner = () => { const field = useField(); const { dn } = useDesignable(); const sortFields = useSortFields(name); - const record = useRecord(); + const { componentNamePrefix } = useBlockTemplateContext(); const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; - const columnCount = field.decoratorProps.columnCount || defaultColumnCount; const columnCountSchema = useMemo(() => { return { @@ -217,7 +216,11 @@ export const GridCardDesigner = () => { }); }} /> - + { const field = useField(); const { dn } = useDesignable(); const sortFields = useSortFields(name); - const record = useRecord(); const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; @@ -57,6 +56,7 @@ export const ListDesigner = () => { direction: 'asc', }; }); + const { componentNamePrefix } = useBlockTemplateContext(); return ( @@ -187,7 +187,11 @@ export const ListDesigner = () => { }); }} /> - + { - const { params } = useCurrentPopupContext(); - const { closePopup } = usePagePopup(); + const { setVisible } = useActionContext(); const goBack = useCallback(() => { - closePopup(params?.popupuid); - }, [closePopup, params?.popupuid]); + setVisible?.(false); + }, [setVisible]); return { goBack, diff --git a/packages/core/client/src/schema-component/antd/page/index.ts b/packages/core/client/src/schema-component/antd/page/index.ts index 83cef7dae2..db761fc2f3 100644 --- a/packages/core/client/src/schema-component/antd/page/index.ts +++ b/packages/core/client/src/schema-component/antd/page/index.ts @@ -13,4 +13,5 @@ export * from './FixedBlockDesignerItem'; export * from './Page'; export * from './Page.Settings'; export { PagePopups } from './PagePopups'; +export { storePopupContext } from './pagePopupUtils'; export * from './PageTab.Settings'; diff --git a/packages/core/client/src/schema-component/antd/page/pagePopupUtils.tsx b/packages/core/client/src/schema-component/antd/page/pagePopupUtils.tsx index 96ac0cf9d8..6a0e7dc649 100644 --- a/packages/core/client/src/schema-component/antd/page/pagePopupUtils.tsx +++ b/packages/core/client/src/schema-component/antd/page/pagePopupUtils.tsx @@ -44,6 +44,10 @@ export interface PopupContextStorage extends PopupContext { /** used to refresh data for block */ service?: any; sourceId?: string; + /** + * if true, will not back to the previous path when closing the popup + */ + notBackToPreviousPath?: boolean; } const popupsContextStorage: Record = {}; @@ -53,7 +57,10 @@ export const getStoredPopupContext = (popupUid: string) => { }; /** - * Used to store the context of the current popup when a button is clicked. + * Used to store the context of the current popup. + * + * The context that has already been stored, when displaying the popup, + * will directly retrieve the context information from the cache instead of making an API request. * @param popupUid * @param params */ @@ -108,6 +115,10 @@ export const getPopupPathFromParams = (params: PopupParams) => { return `/popups/${popupPath.map((item) => encodePathValue(item)).join('/')}`; }; +/** + * Note: use this hook in a plugin is not recommended + * @returns + */ export const usePagePopup = () => { const navigate = useNavigateNoUpdate(); const location = useLocationNoUpdate(); @@ -228,13 +239,13 @@ export const usePagePopup = () => { // 1. If there is a value in the cache, it means that the current popup was opened by manual click, so we can simply return to the previous record; // 2. If there is no value in the cache, it means that the current popup was opened by clicking the URL elsewhere, and since there is no history, // we need to construct the URL of the previous record to return to; - if (getStoredPopupContext(currentPopupUid)) { + if (getStoredPopupContext(currentPopupUid) && !getStoredPopupContext(currentPopupUid).notBackToPreviousPath) { navigate(-1); } else { navigate(withSearchParams(removeLastPopupPath(location.pathname))); } }, - [navigate, location, isPopupVisibleControlledByURL], + [isPopupVisibleControlledByURL, setVisibleFromAction, navigate, location?.pathname], ); const changeTab = useCallback( diff --git a/packages/core/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx b/packages/core/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx index 5686635292..73ad7dd0e7 100644 --- a/packages/core/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx +++ b/packages/core/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx @@ -9,6 +9,7 @@ import { ArrayField } from '@formily/core'; import { RecursionField, useField, useFieldSchema } from '@formily/react'; +import { toArr } from '@formily/shared'; import { Select } from 'antd'; import { differenceBy, unionBy } from 'lodash'; import React, { createContext, useContext, useEffect, useState } from 'react'; @@ -20,10 +21,9 @@ import { CollectionProvider_deprecated, useCollection_deprecated } from '../../. import { FormProvider, SchemaComponentOptions } from '../../core'; import { useCompile } from '../../hooks'; import { ActionContextProvider, useActionContext } from '../action'; +import { Upload } from '../upload'; import { useFieldNames } from './useFieldNames'; import { getLabelFormatValue, useLabelUiSchema } from './util'; -import { Upload } from '../upload'; -import { toArr } from '@formily/shared'; export const RecordPickerContext = createContext(null); RecordPickerContext.displayName = 'RecordPickerContext'; @@ -148,11 +148,6 @@ export const InputRecordPicker: React.FC = (props: IRecordPickerProps) => { return Array.isArray(value) ? value?.map((v) => v[fieldNames.value]) : value?.[fieldNames.value]; }; - const handleSelect = () => { - setVisible(true); - setSelectedRows([]); - }; - // const handleRemove = (file) => { // const newOptions = options.filter((option) => option.id !== file.id); // setOptions(newOptions); diff --git a/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx b/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx index 7dca15e2bc..1f68749e1c 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx @@ -35,6 +35,7 @@ import { SchemaSettingsConnectDataBlocks } from '../../../schema-settings/Schema import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; import { useSchemaTemplate } from '../../../schema-templates'; +import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider'; import { useDesignable } from '../../hooks'; import { removeNullCondition } from '../filter'; @@ -86,6 +87,7 @@ export const TableBlockDesigner = () => { const { service } = useTableBlockContext(); const { t } = useTranslation(); const { dn } = useDesignable(); + const { componentNamePrefix } = useBlockTemplateContext(); const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; const defaultResource = @@ -304,7 +306,11 @@ export const TableBlockDesigner = () => { {supportTemplate && } {supportTemplate && ( - + )} { @@ -48,6 +49,7 @@ export const TableVoidDesigner = () => { }; }); const template = useSchemaTemplate(); + const { componentNamePrefix } = useBlockTemplateContext(); return ( { }} /> - + { @@ -140,7 +140,7 @@ function useRecordBlocks() { onlyCurrentDataSource: true, hideSearch: true, hideOtherRecordsInPopup: true, - componentType: 'FormItem', + componentType: `FormItem`, createBlockSchema: createEditFormBlock, templateWrap: templateWrapEdit, showAssociationFields: true, @@ -166,7 +166,7 @@ function useRecordBlocks() { }, onlyCurrentDataSource: true, hideSearch: true, - componentType: 'FormItem', + componentType: `FormItem`, createBlockSchema: ({ item, fromOthersInPopup }) => { if (fromOthersInPopup) { return createFormBlock({ item, fromOthersInPopup }); diff --git a/packages/core/client/src/schema-initializer/buttons/TabPaneInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TabPaneInitializers.tsx index 4dcc464ed5..ae5e08d913 100644 --- a/packages/core/client/src/schema-initializer/buttons/TabPaneInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TabPaneInitializers.tsx @@ -81,6 +81,7 @@ const TabPaneInitializers = (props?: any) => { 'x-component': 'Action.Modal', 'x-component-props': { width: 520, + zIndex: 2000, }, type: 'void', title: '{{t("Add tab")}}', diff --git a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx index 289bc6d5a0..c3e2d3e5b1 100644 --- a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx +++ b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx @@ -271,7 +271,7 @@ function FinallyButton({ }), React.cloneElement(rightButton as React.ReactElement, { loading: false, - style: props?.style, + style: { ...props?.style, justifyContent: 'center' }, }), ]} menu={menu} diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index b89f7c9a70..bd0e03ef3d 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -30,6 +30,7 @@ import { useDataSourceManager } from '../data-source/data-source/DataSourceManag import { isAssocField } from '../filter-provider/utils'; import { useActionContext, useCompile, useDesignable } from '../schema-component'; import { useSchemaTemplateManager } from '../schema-templates'; +import { useBlockTemplateContext } from '../schema-templates/BlockTemplateProvider'; export const itemsMerge = (items1) => { return items1; @@ -879,11 +880,17 @@ export const useCollectionDataSourceItems = ({ currentText?: string; otherText?: string; }) => { + const { componentNamePrefix } = useBlockTemplateContext(); const { t } = useTranslation(); const dm = useDataSourceManager(); const dataSourceKey = useDataSourceKey(); const collection = useCollection(); - const associationFields = useAssociationFields({ componentName, filterCollections: filter, showAssociationFields }); + const associationFields = useAssociationFields({ + componentName: componentNamePrefix + componentName, + filterCollections: filter, + showAssociationFields, + componentNamePrefix, + }); const association = useAssociationName(); let allCollections = dm.getAllCollections({ @@ -911,11 +918,12 @@ export const useCollectionDataSourceItems = ({ name, association, collections, - componentName, + componentName: componentNamePrefix + componentName, searchValue: '', dataSource: key, getTemplatesByCollection, t, + componentNamePrefix, }).sort((item) => { // fix https://nocobase.height.app/T-3551 const inherits = _.toArray(collection?.inherits || []); @@ -1401,6 +1409,7 @@ const getChildren = ({ searchValue, getTemplatesByCollection, t, + componentNamePrefix, }: { name: string; association: string; @@ -1409,7 +1418,8 @@ const getChildren = ({ searchValue: string; dataSource: string; getTemplatesByCollection: (dataSource: string, collectionName: string, resourceName?: string) => any; - t; + t: any; + componentNamePrefix: string; }) => { return collections ?.filter((item) => { @@ -1419,11 +1429,16 @@ const getChildren = ({ if (!item.filterTargetKey) { return false; } else if ( - ['Kanban', 'FormItem'].includes(componentName) && + [componentNamePrefix + 'Kanban', componentNamePrefix + 'FormItem'].includes(componentName) && ((item.template === 'view' && !item.writableView) || item.template === 'sql') ) { return false; - } else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) { + } else if ( + item.template === 'file' && + [componentNamePrefix + 'Kanban', componentNamePrefix + 'FormItem', componentNamePrefix + 'Calendar'].includes( + componentName, + ) + ) { return false; } else { const title = item.title || item.tableName; @@ -1483,7 +1498,10 @@ const getChildren = ({ dataSource, title: t('Duplicate template'), children: templates.map((template) => { - const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) + const templateName = [ + componentNamePrefix + 'FormItem', + componentNamePrefix + 'ReadPrettyFormItem', + ].includes(template?.componentName) ? `${template?.name} ${t('(Fields only)')}` : template?.name; return { @@ -1503,7 +1521,10 @@ const getChildren = ({ dataSource, title: t('Reference template'), children: templates.map((template) => { - const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) + const templateName = [ + componentNamePrefix + 'FormItem', + componentNamePrefix + 'ReadPrettyFormItem', + ].includes(template?.componentName) ? `${template?.name} ${t('(Fields only)')}` : template?.name; return { @@ -1525,9 +1546,11 @@ function useAssociationFields({ componentName, filterCollections, showAssociationFields, + componentNamePrefix, }: { componentName: string; filterCollections: (options: { collection?: Collection; associationField?: CollectionFieldOptions }) => boolean; + componentNamePrefix: string; showAssociationFields?: boolean; }) { const fieldSchema = useFieldSchema(); @@ -1566,11 +1589,11 @@ function useAssociationFields({ } // 针对弹窗中的详情区块 - if (componentName === 'ReadPrettyFormItem') { + if (componentName === componentNamePrefix + 'ReadPrettyFormItem') { if (['hasOne', 'belongsTo'].includes(field.type)) { - return template.componentName === 'ReadPrettyFormItem'; + return template.componentName === componentNamePrefix + 'ReadPrettyFormItem'; } else { - return template.componentName === 'Details'; + return template.componentName === componentNamePrefix + 'Details'; } } @@ -1611,7 +1634,10 @@ function useAssociationFields({ dataSource, title: t('Duplicate template'), children: templates.map((template) => { - const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) + const templateName = [ + componentNamePrefix + 'FormItem', + componentNamePrefix + 'ReadPrettyFormItem', + ].includes(template?.componentName) ? `${template?.name} ${t('(Fields only)')}` : template?.name; return { @@ -1633,7 +1659,10 @@ function useAssociationFields({ dataSource, title: t('Reference template'), children: templates.map((template) => { - const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) + const templateName = [ + componentNamePrefix + 'FormItem', + componentNamePrefix + 'ReadPrettyFormItem', + ].includes(template?.componentName) ? `${template?.name} ${t('(Fields only)')}` : template?.name; return { @@ -1662,5 +1691,6 @@ function useAssociationFields({ getTemplatesByCollection, showAssociationFields, t, + componentNamePrefix, ]); } diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index 53c96b5ee2..70cde8940c 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -95,7 +95,7 @@ import { SchemaComponentOptions } from '../schema-component/core/SchemaComponent 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/BlockTemplate'; +import { useBlockTemplateContext } from '../schema-templates/BlockTemplateProvider'; import { useLocalVariables, useVariables } from '../variables'; import { FormDataTemplates } from './DataTemplates'; import { EnableChildCollections } from './EnableChildCollections'; diff --git a/packages/core/client/src/schema-settings/SchemaSettingsTemplate.tsx b/packages/core/client/src/schema-settings/SchemaSettingsTemplate.tsx index 2a25d0cf6b..aaaf338cff 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsTemplate.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettingsTemplate.tsx @@ -20,7 +20,7 @@ import { SchemaComponent } from '../schema-component/core/SchemaComponent'; import { useCompile } from '../schema-component/hooks/useCompile'; import { createDesignable } from '../schema-component/hooks/useDesignable'; import { useSchemaTemplateManager } from '../schema-templates'; -import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; +import { useBlockTemplateContext } from '../schema-templates/BlockTemplateProvider'; import { SchemaSettingsItem, useSchemaSettings } from './SchemaSettings'; export function SchemaSettingsTemplate(props) { diff --git a/packages/core/client/src/schema-templates/BlockTemplate.tsx b/packages/core/client/src/schema-templates/BlockTemplate.tsx index 389cecad7c..1f091640dd 100644 --- a/packages/core/client/src/schema-templates/BlockTemplate.tsx +++ b/packages/core/client/src/schema-templates/BlockTemplate.tsx @@ -8,17 +8,10 @@ */ import { observer, useField, useFieldSchema } from '@formily/react'; -import React, { createContext, useContext, useMemo } from 'react'; -import { CollectionDeletedPlaceholder, RemoteSchemaComponent, useDesignable } from '..'; -import { useSchemaTemplateManager } from './SchemaTemplateManagerProvider'; +import React, { useMemo } from 'react'; +import { BlockTemplateProvider, CollectionDeletedPlaceholder, RemoteSchemaComponent, useDesignable } from '..'; import { useTemplateBlockContext } from '../block-provider/TemplateBlockProvider'; - -const BlockTemplateContext = createContext({}); -BlockTemplateContext.displayName = 'BlockTemplateContext'; - -export const useBlockTemplateContext = () => { - return useContext(BlockTemplateContext); -}; +import { useSchemaTemplateManager } from './SchemaTemplateManagerProvider'; export const BlockTemplate = observer( (props: any) => { @@ -36,9 +29,9 @@ export const BlockTemplate = observer( onTemplateSuccess?.(); }; return template ? ( - + - + ) : ( ); diff --git a/packages/core/client/src/schema-templates/BlockTemplateProvider.tsx b/packages/core/client/src/schema-templates/BlockTemplateProvider.tsx new file mode 100644 index 0000000000..ae4d84618b --- /dev/null +++ b/packages/core/client/src/schema-templates/BlockTemplateProvider.tsx @@ -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 { ISchema } from '@formily/json-schema'; +import React, { createContext, FC, useContext } from 'react'; + +interface BlockTemplateProviderProps { + /** + * 为模板中的 componentName 参数设置一个前缀,用于实现相同区块的模板在不同的上下文中不会互相引用 + */ + componentNamePrefix?: string; + dn?: any; + field?: any; + fieldSchema?: ISchema; + template?: any; +} + +export const BlockTemplateContext = createContext({ componentNamePrefix: '' }); + +export const BlockTemplateProvider: FC = (props) => { + return ( + + {props.children} + + ); +}; +export const useBlockTemplateContext = () => { + return useContext(BlockTemplateContext); +}; diff --git a/packages/core/client/src/schema-templates/index.ts b/packages/core/client/src/schema-templates/index.ts index cae0b019ee..f3ddb801a0 100644 --- a/packages/core/client/src/schema-templates/index.ts +++ b/packages/core/client/src/schema-templates/index.ts @@ -9,4 +9,5 @@ export * from './BlockTemplateDetails'; export * from './BlockTemplatePage'; +export * from './BlockTemplateProvider'; export * from './SchemaTemplateManagerProvider'; diff --git a/packages/core/utils/src/client.ts b/packages/core/utils/src/client.ts index a3f82e0d69..c58b8e32fc 100644 --- a/packages/core/utils/src/client.ts +++ b/packages/core/utils/src/client.ts @@ -24,9 +24,9 @@ export * from './number'; export * from './parse-filter'; export * from './registry'; // export * from './toposort'; +export * from './i18n'; export * from './isPortalInBody'; +export * from './parseHTML'; export * from './uid'; export * from './url'; export { dayjs, lodash }; -export * from './parseHTML'; -export * from './i18n'; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/menu.test.ts b/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/menu.test.ts index 629027dd87..48dbc87d0e 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/menu.test.ts +++ b/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/menu.test.ts @@ -70,7 +70,7 @@ test('menu permission ', async ({ page, mockPage, mockRole, updateRole }) => { }); test('i18n should not fallbackNS', async ({ page }) => { - await page.goto('/admin/settings/system-settings'); + await page.goto('/'); // 创建 Users 页面 await page.getByTestId('schema-initializer-Menu-header').hover(); @@ -78,11 +78,12 @@ test('i18n should not fallbackNS', async ({ page }) => { await page.getByLabel('block-item-Input-Menu item').getByRole('textbox').click(); await page.getByLabel('block-item-Input-Menu item').getByRole('textbox').fill('Users'); await page.getByRole('button', { name: 'OK' }).click(); - await expect(page.getByLabel('Users')).toBeVisible(); + await page.getByLabel('Users').first().click(); + await expect(page.getByLabel('Users').first()).toBeVisible(); await expect(page.getByLabel('用户')).not.toBeVisible(); // 添加中文选项 - await page.reload(); + await page.goto('/admin/settings/system-settings'); await page.getByTestId('select-multiple').click(); await page.getByRole('option', { name: '简体中文 (zh-CN)' }).click(); await page.getByLabel('action-Action-Submit').click(); @@ -92,19 +93,18 @@ test('i18n should not fallbackNS', async ({ page }) => { await page.getByText('LanguageEnglish').click(); await page.getByRole('option', { name: '简体中文' }).click(); - // await page.reload(); - // 应该显示 Users 而非中文 “用户” - await expect(page.getByLabel('Users')).toBeVisible(); + await expect(page.getByLabel('Users').first()).toBeVisible(); await expect(page.getByLabel('用户')).not.toBeVisible(); // 删除中文 + await page.goto('/admin/settings/system-settings'); await page.getByLabel('简体中文 (zh-CN)').getByLabel('icon-close-tag').click(); await page.getByLabel('action-Action-提交').click(); // 删除 Users 页面 - await page.getByLabel('Users').hover(); - await page.getByLabel('designer-schema-settings-Menu').hover(); + await page.getByLabel('Users').first().hover(); + await page.getByLabel('designer-schema-settings-Menu').first().hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('button', { name: 'OK' }).click(); }); diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/BulkEditActionInitializer.tsx b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/BulkEditActionInitializer.tsx index b6594bf127..860ac07566 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/BulkEditActionInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/BulkEditActionInitializer.tsx @@ -7,10 +7,12 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { BlockInitializer, useSchemaInitializerItem } from '@nocobase/client'; +import { BlockInitializer, useOpenModeContext, useSchemaInitializerItem } from '@nocobase/client'; import React from 'react'; export const BulkEditActionInitializer = () => { + const { defaultOpenMode } = useOpenModeContext(); + const schema = { type: 'void', title: '{{t("Bulk edit")}}', @@ -20,7 +22,7 @@ export const BulkEditActionInitializer = () => { updateMode: 'selected', }, 'x-component-props': { - openMode: 'drawer', + openMode: defaultOpenMode, icon: 'EditOutlined', }, properties: { diff --git a/packages/plugins/@nocobase/plugin-action-duplicate/src/client/DuplicateActionInitializer.tsx b/packages/plugins/@nocobase/plugin-action-duplicate/src/client/DuplicateActionInitializer.tsx index ac5bc3794c..fd18e8a8af 100644 --- a/packages/plugins/@nocobase/plugin-action-duplicate/src/client/DuplicateActionInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-action-duplicate/src/client/DuplicateActionInitializer.tsx @@ -7,10 +7,12 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { ActionInitializerItem } from '@nocobase/client'; +import { ActionInitializerItem, useOpenModeContext } from '@nocobase/client'; import React from 'react'; export const DuplicateActionInitializer = (props) => { + const { defaultOpenMode } = useOpenModeContext(); + const schema = { type: 'void', 'x-action': 'duplicate', @@ -19,7 +21,7 @@ export const DuplicateActionInitializer = (props) => { 'x-component': 'Action.Link', 'x-decorator': 'ACLActionProvider', 'x-component-props': { - openMode: 'drawer', + openMode: defaultOpenMode, component: 'DuplicateAction', }, properties: { diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/__tests__/createCalendarBlockSchema.test.ts b/packages/plugins/@nocobase/plugin-calendar/src/client/__tests__/createCalendarBlockSchema.test.ts index 7c7d933216..b3e004d52e 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/__tests__/createCalendarBlockSchema.test.ts +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/__tests__/createCalendarBlockSchema.test.ts @@ -72,7 +72,7 @@ describe('createCalendarBlockSchema', () => { }, "title": "{{t('View record', { ns: 'calendar' })}}", "type": "void", - "x-component": "Action.Drawer", + "x-component": "Action.Container", "x-component-props": { "className": "nb-action-popup", }, diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx index f6d186d9dd..bead118e6a 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx @@ -18,6 +18,7 @@ import { SchemaSettingsSwitchItem, SchemaSettingsTemplate, removeNullCondition, + useBlockTemplateContext, useCollection, useCollectionManager_deprecated, useDesignable, @@ -212,10 +213,11 @@ export const calendarBlockSettings = new SchemaSettings({ useComponentProps() { const { name } = useCollection(); const fieldSchema = useFieldSchema(); + const { componentNamePrefix } = useBlockTemplateContext(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { - componentName: 'Calendar', + componentName: `${componentNamePrefix}Calendar`, collectionName: name, resourceName: defaultResource, }; diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/createCalendarBlockUISchema.ts b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/createCalendarBlockUISchema.ts index cf83149ca5..45e78e43a9 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/createCalendarBlockUISchema.ts +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/createCalendarBlockUISchema.ts @@ -62,7 +62,7 @@ export const createCalendarBlockUISchema = (options: { properties: { drawer: { type: 'void', - 'x-component': 'Action.Drawer', + 'x-component': 'Action.Container', 'x-component-props': { className: 'nb-action-popup', }, diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx index 844436022a..7b1e008c22 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx @@ -45,7 +45,7 @@ export const CalendarBlockInitializer = ({ return ( } onCreateBlockSchema={async (options) => { if (createBlockSchema) { diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/Gantt.Settings.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/Gantt.Settings.tsx index d86d5a9fd9..2916d9128f 100644 --- a/packages/plugins/@nocobase/plugin-gantt/src/client/Gantt.Settings.tsx +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/Gantt.Settings.tsx @@ -16,6 +16,7 @@ import { SchemaSettingsTemplate, removeNullCondition, setDataLoadingModeSettingsItem, + useBlockTemplateContext, useCollection, useCollection_deprecated, useCompile, @@ -245,8 +246,9 @@ export const oldGanttSettings = new SchemaSettings({ Component: SchemaSettingsTemplate, useComponentProps() { const { name } = useCollection_deprecated(); + const { componentNamePrefix } = useBlockTemplateContext(); return { - componentName: 'Gantt', + componentName: `${componentNamePrefix}Gantt`, collectionName: name, }; }, @@ -485,8 +487,9 @@ export const ganttSettings = new SchemaSettings({ Component: SchemaSettingsTemplate, useComponentProps() { const { name } = useCollection(); + const { componentNamePrefix } = useBlockTemplateContext(); return { - componentName: 'Gantt', + componentName: `${componentNamePrefix}Gantt`, collectionName: name, }; }, diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx index c905a7e4fa..310b65638b 100644 --- a/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx @@ -14,16 +14,16 @@ import React, { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { - useSchemaInitializer, - useSchemaInitializerItem, - useCollectionManager_deprecated, - useGlobalTheme, - FormDialog, - SchemaComponent, - DataBlockInitializer, - SchemaComponentOptions, Collection, CollectionFieldOptions, + DataBlockInitializer, + FormDialog, + SchemaComponent, + SchemaComponentOptions, + useCollectionManager_deprecated, + useGlobalTheme, + useSchemaInitializer, + useSchemaInitializerItem, } from '@nocobase/client'; import { createGanttBlockUISchema } from './createGanttBlockUISchema'; @@ -46,7 +46,7 @@ export const GanttBlockInitializer = ({ return ( } onCreateBlockSchema={async (options) => { if (createBlockSchema) { diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Settings.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Settings.tsx index 0d90c95725..02d76b72f6 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Settings.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Settings.tsx @@ -9,15 +9,17 @@ import { useField, useFieldSchema } from '@formily/react'; import { - useFormBlockContext, + removeNullCondition, + SchemaSettings, + SchemaSettingsBlockHeightItem, + SchemaSettingsBlockTitleItem, SchemaSettingsDataScope, + SchemaSettingsTemplate, + useBlockTemplateContext, + useCollection, useCollection_deprecated, useDesignable, - SchemaSettings, - SchemaSettingsBlockTitleItem, - removeNullCondition, - SchemaSettingsTemplate, - SchemaSettingsBlockHeightItem, + useFormBlockContext, } from '@nocobase/client'; import { useKanbanBlockContext } from './KanbanBlockProvider'; export const kanbanSettings = new SchemaSettings({ @@ -66,9 +68,10 @@ export const kanbanSettings = new SchemaSettings({ name: 'template', Component: SchemaSettingsTemplate, useComponentProps() { - const { name } = useCollection_deprecated(); + const { name } = useCollection(); + const { componentNamePrefix } = useBlockTemplateContext(); return { - componentName: 'Kanban', + componentName: `${componentNamePrefix}Kanban`, collectionName: name, }; }, diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockInitializer.tsx index ff4891fb3c..d7f959f30e 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockInitializer.tsx @@ -15,20 +15,20 @@ import { useTranslation } from 'react-i18next'; import { APIClientProvider, - useCollectionManager_deprecated, - useGlobalTheme, + Collection, + CollectionFieldOptions, + DataBlockInitializer, FormDialog, SchemaComponent, SchemaComponentOptions, - DataBlockInitializer, + useAPIClient, + useCollectionManager_deprecated, + useGlobalTheme, useSchemaInitializer, useSchemaInitializerItem, - useAPIClient, - Collection, - CollectionFieldOptions, } from '@nocobase/client'; -import { createKanbanBlockUISchema } from './createKanbanBlockUISchema'; import { CreateAndSelectSort } from './CreateAndSelectSort'; +import { createKanbanBlockUISchema } from './createKanbanBlockUISchema'; import { NAMESPACE } from './locale'; const CreateKanbanForm = ({ item, sortFields, collectionFields, fields, options, api }) => { @@ -130,7 +130,7 @@ export const KanbanBlockInitializer = ({ return ( } onCreateBlockSchema={async (options) => { if (createBlockSchema) { diff --git a/packages/plugins/@nocobase/plugin-map/src/client/__tests__/block/createMapBlockSchema.test.ts b/packages/plugins/@nocobase/plugin-map/src/client/__tests__/block/createMapBlockSchema.test.ts index 7e0d8decad..82af666a05 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/__tests__/block/createMapBlockSchema.test.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/__tests__/block/createMapBlockSchema.test.ts @@ -65,7 +65,7 @@ test('createMapBlockSchema should return an object with expected properties', () }, "title": "{{ t("View record") }}", "type": "void", - "x-component": "Action.Drawer", + "x-component": "Action.Container", "x-component-props": { "className": "nb-action-popup", }, diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.Settings.tsx b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.Settings.tsx index da02eaa251..f373cfc0c3 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.Settings.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.Settings.tsx @@ -11,6 +11,7 @@ import { ISchema, useField, useFieldSchema } from '@formily/react'; import { FilterBlockType, SchemaSettings, + SchemaSettingsBlockHeightItem, SchemaSettingsBlockTitleItem, SchemaSettingsCascaderItem, SchemaSettingsConnectDataBlocks, @@ -20,11 +21,11 @@ import { SchemaSettingsSelectItem, SchemaSettingsTemplate, setDataLoadingModeSettingsItem, + useBlockTemplateContext, useCollection, useCollectionManager_deprecated, useDesignable, useFormBlockContext, - SchemaSettingsBlockHeightItem, } from '@nocobase/client'; import _ from 'lodash'; import { useMapTranslation } from '../locale'; @@ -231,10 +232,11 @@ export const mapBlockSettings = new SchemaSettings({ useComponentProps() { const { name } = useCollection(); const fieldSchema = useFieldSchema(); + const { componentNamePrefix } = useBlockTemplateContext(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; return { - componentName: 'Map', + componentName: `${componentNamePrefix}Map`, collectionName: name, resourceName: defaultResource, }; diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx index 32dc969ad7..793c7ceb4f 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx @@ -22,8 +22,8 @@ import { } from '@nocobase/client'; import React, { useContext } from 'react'; import { useMapTranslation } from '../locale'; -import { findNestedOption } from './utils'; import { createMapBlockUISchema } from './createMapBlockUISchema'; +import { findNestedOption } from './utils'; export const MapBlockInitializer = () => { const itemConfig = useSchemaInitializerItem(); @@ -32,9 +32,10 @@ export const MapBlockInitializer = () => { const { getCollectionFieldsOptions } = useCollectionManager_deprecated(); const { t } = useMapTranslation(); const { theme } = useGlobalTheme(); + return ( } onCreateBlockSchema={async ({ item }) => { const mapFieldOptions = getCollectionFieldsOptions(item.name, ['point', 'lineString', 'polygon'], { diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/createMapBlockUISchema.ts b/packages/plugins/@nocobase/plugin-map/src/client/block/createMapBlockUISchema.ts index 7594d6f60e..64edcf8898 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/createMapBlockUISchema.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/createMapBlockUISchema.ts @@ -9,7 +9,6 @@ import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; -import { theme } from 'antd'; export const createMapBlockUISchema = (options: { collectionName: string; @@ -49,7 +48,7 @@ export const createMapBlockUISchema = (options: { properties: { drawer: { type: 'void', - 'x-component': 'Action.Drawer', + 'x-component': 'Action.Container', 'x-component-props': { className: 'nb-action-popup', }, diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Block.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Block.tsx index ae394a614a..50e5d8ed70 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Block.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Block.tsx @@ -326,7 +326,6 @@ export const AMapBlock = (props) => { const MapBlockDrawer = (props) => { const { setVisible, record } = props; - const { t } = useMapTranslation(); const collection = useCollection(); const parentRecordData = useCollectionParentRecordData(); const fieldSchema = useFieldSchema(); diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Block.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Block.tsx index f2d19cb7af..716768183f 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Block.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Block.tsx @@ -375,7 +375,6 @@ export const GoogleMapsBlock = (props) => { const MapBlockDrawer = (props) => { const { setVisible, record } = props; - const { t } = useMapTranslation(); const collection = useCollection(); const parentRecordData = useCollectionParentRecordData(); const fieldSchema = useFieldSchema(); diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/ActionDrawer.style.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/ActionDrawer.style.ts new file mode 100644 index 0000000000..45c56276f2 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/ActionDrawer.style.ts @@ -0,0 +1,67 @@ +/** + * 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 { createStyles } from '@nocobase/client'; + +export const useMobileActionDrawerStyle = createStyles(({ css, token }: any) => { + return { + header: css` + height: var(--nb-mobile-page-header-height); + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid ${token.colorSplit}; + position: sticky; + top: 0; + background-color: white; + z-index: 1000; + + // to match the button named 'Add block' + & + .nb-grid-container > .nb-grid > .nb-grid-warp > .ant-btn { + // 18px is the token marginBlock value + margin: 12px 12px calc(12px + 18px); + } + `, + + placeholder: css` + display: inline-block; + padding: 12px; + visibility: hidden; + `, + + closeIcon: css` + display: inline-block; + padding: 12px; + cursor: pointer; + `, + + body: css` + border-top-left-radius: 8px; + border-top-right-radius: 8px; + max-height: calc(100% - var(--nb-mobile-page-header-height)); + overflow-y: auto; + overflow-x: hidden; + background-color: ${token.colorBgLayout}; + `, + + footer: css` + padding: 8px var(--nb-mobile-page-tabs-content-padding); + display: flex; + align-items: center; + justify-content: flex-end; + position: sticky; + bottom: 0; + left: 0; + right: 0; + z-index: 1000; + border-top: 1px solid ${token.colorSplit}; + background-color: ${token.colorBgLayout}; + `, + }; +}); diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/ActionDrawer.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/ActionDrawer.tsx new file mode 100644 index 0000000000..5dad502e64 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/ActionDrawer.tsx @@ -0,0 +1,127 @@ +/** + * 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 { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; +import { Action, SchemaComponent, useActionContext } from '@nocobase/client'; +import { ConfigProvider } from 'antd'; +import { Popup } from 'antd-mobile'; +import { CloseOutline } from 'antd-mobile-icons'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import { useMobileActionDrawerStyle } from './ActionDrawer.style'; +import { BasicZIndexProvider, MIN_Z_INDEX_INCREMENT, useBasicZIndex } from './BasicZIndexProvider'; +import { usePopupContainer } from './FilterAction'; + +export const ActionDrawerUsedInMobile = observer((props: { footerNodeName?: string }) => { + const fieldSchema = useFieldSchema(); + const field = useField(); + const { visible, setVisible } = useActionContext(); + const { popupContainerRef, visiblePopup } = usePopupContainer(visible); + const { styles } = useMobileActionDrawerStyle(); + const { basicZIndex } = useBasicZIndex(); + + const newZIndex = basicZIndex + MIN_Z_INDEX_INCREMENT; + + const zIndexStyle = useMemo(() => { + return { + zIndex: newZIndex, + }; + }, [newZIndex]); + + const footerSchema = fieldSchema.reduceProperties((buf, s) => { + if (s['x-component'] === props.footerNodeName) { + return s; + } + return buf; + }); + + const title = field.title || ''; + const marginBlock = 18; + + const closePopup = useCallback(() => { + setVisible(false); + }, [setVisible]); + + const theme = useMemo(() => { + return { + token: { + marginBlock, + borderRadiusBlock: 0, + boxShadowTertiary: 'none', + zIndexPopupBase: newZIndex, + }, + }; + }, [newZIndex]); + + return ( + + + popupContainerRef.current} + bodyClassName={styles.body} + bodyStyle={zIndexStyle} + maskStyle={zIndexStyle} + closeOnSwipe + > +
+ {/* used to make the title center */} + + + + {title} + + + +
+ { + return s['x-component'] !== props.footerNodeName; + }} + /> + {/* used to offset the margin-bottom of the last block */} + {/* The number 1 is to prevent the scroll bar from appearing */} +
+ {footerSchema ? ( +
+ { + return s['x-component'] === props.footerNodeName; + }} + /> +
+ ) : null} +
+
+
+ ); +}); + +ActionDrawerUsedInMobile.displayName = 'ActionDrawerUsedInMobile'; + +const originalActionDrawer = Action.Drawer; + +/** + * adapt Action.Drawer to mobile + */ +export const useToAdaptActionDrawerToMobile = () => { + Action.Drawer = ActionDrawerUsedInMobile; + + useEffect(() => { + return () => { + Action.Drawer = originalActionDrawer; + }; + }, []); +}; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/BasicZIndexProvider.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/BasicZIndexProvider.tsx new file mode 100644 index 0000000000..c802e0d3d1 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/BasicZIndexProvider.tsx @@ -0,0 +1,33 @@ +/** + * 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 React, { useMemo } from 'react'; + +const BasicZIndexContext = React.createContext<{ + basicZIndex: number; +}>({ + basicZIndex: 0, +}); + +/** + * used to accumulate z-index in nested popups + * @param props + * @returns + */ +export const BasicZIndexProvider: React.FC<{ basicZIndex: number }> = (props) => { + const value = useMemo(() => ({ basicZIndex: props.basicZIndex }), [props.basicZIndex]); + return {props.children}; +}; + +export const useBasicZIndex = () => { + return React.useContext(BasicZIndexContext); +}; + +// minimum z-index increment +export const MIN_Z_INDEX_INCREMENT = 10; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/FilterAction.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/FilterAction.tsx new file mode 100644 index 0000000000..7975881223 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/FilterAction.tsx @@ -0,0 +1,166 @@ +/** + * 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 { Filter, withDynamicSchemaProps } from '@nocobase/client'; +import { ConfigProvider } from 'antd'; +import { Popup } from 'antd-mobile'; +import { CloseOutline } from 'antd-mobile-icons'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMobileActionDrawerStyle } from './ActionDrawer.style'; +import { MIN_Z_INDEX_INCREMENT, useBasicZIndex } from './BasicZIndexProvider'; + +const OriginFilterAction = Filter.Action; + +export const FilterAction = withDynamicSchemaProps((props) => { + return ( + { + const { visiblePopup, popupContainerRef } = usePopupContainer(props.open); + const { basicZIndex } = useBasicZIndex(); + const { styles } = useMobileActionDrawerStyle(); + const { t } = useTranslation(); + + const newZIndex = basicZIndex + MIN_Z_INDEX_INCREMENT; + + // eslint-disable-next-line react-hooks/rules-of-hooks + const closePopup = useCallback(() => { + props.onOpenChange(false); + }, [props]); + + const theme = useMemo(() => { + return { + token: { + zIndexPopupBase: newZIndex, + }, + }; + }, [newZIndex]); + + const bodyStyle = useMemo( + () => ({ + borderTopLeftRadius: '8px', + borderTopRightRadius: '8px', + maxHeight: 'calc(100% - var(--nb-mobile-page-header-height))', + overflow: 'auto', + zIndex: newZIndex, + }), + [newZIndex], + ); + + const zIndexStyle = useMemo(() => ({ zIndex: newZIndex }), [newZIndex]); + + const getContainer = useCallback(() => popupContainerRef.current, [popupContainerRef]); + + return ( + + {props.children} + +
+ {/* used to make the title center */} + + + + {t('Filter')} + + + +
+
{props.content}
+
+
+
+ ); + }} + /> + ); +}); + +FilterAction.displayName = 'FilterAction'; + +const originalFilterAction = Filter.Action; + +/** + * adapt Filter.Action to mobile + */ +export const useToAdaptFilterActionToMobile = () => { + Filter.Action = FilterAction; + + useEffect(() => { + return () => { + Filter.Action = originalFilterAction; + }; + }, []); +}; + +/** + * 之所以不直接使用 mobile-container 作为容器,是因为会影响到区块的拖拽功能。详见:https://nocobase.height.app/T-4959 + * @param visible + * @returns + */ +export const usePopupContainer = (visible: boolean) => { + const [mobileContainer] = useState(() => document.querySelector('.mobile-container')); + const [visiblePopup, setVisiblePopup] = useState(false); + const popupContainerRef = React.useRef(null); + const { basicZIndex } = useBasicZIndex(); + + const newZIndex = basicZIndex + MIN_Z_INDEX_INCREMENT; + + useEffect(() => { + if (!visible) { + setVisiblePopup(false); + if (popupContainerRef.current) { + // Popup 动画都结束的时候再移除 + setTimeout(() => { + mobileContainer.contains(popupContainerRef.current) && mobileContainer.removeChild(popupContainerRef.current); + popupContainerRef.current = null; + }, 300); + } + return; + } + + const popupContainer = document.createElement('div'); + popupContainer.style.transform = 'translateZ(0)'; + popupContainer.style.position = 'absolute'; + popupContainer.style.top = '0'; + popupContainer.style.left = '0'; + popupContainer.style.right = '0'; + popupContainer.style.bottom = '0'; + popupContainer.style.overflow = 'hidden'; + popupContainer.style.zIndex = newZIndex.toString(); + + mobileContainer.appendChild(popupContainer); + popupContainerRef.current = popupContainer; + + setVisiblePopup(true); + + return () => { + if (popupContainerRef.current) { + // Popup 动画都结束的时候再移除 + setTimeout(() => { + mobileContainer.contains(popupContainerRef.current) && mobileContainer.removeChild(popupContainerRef.current); + popupContainerRef.current = null; + }, 300); + } + }; + }, [mobileContainer, newZIndex, visible]); + + return { + visiblePopup, + popupContainerRef, + }; +}; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/InternalPopoverNester.style.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/InternalPopoverNester.style.ts new file mode 100644 index 0000000000..d8ec410ae7 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/InternalPopoverNester.style.ts @@ -0,0 +1,77 @@ +/** + * 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 { createStyles } from '@nocobase/client'; + +export const useInternalPopoverNesterUsedInMobileStyle = createStyles(({ css, token }: any) => { + return { + header: css` + height: var(--nb-mobile-page-header-height); + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid ${token.colorSplit}; + position: sticky; + top: 0; + background-color: white; + z-index: 1000; + + // to match the button named 'Add block' + & + .nb-grid-container > .nb-grid > .nb-grid-warp > .ant-btn { + // 18px is the token marginBlock value + margin: 12px 12px calc(12px + 18px); + } + `, + + placeholder: css` + display: inline-block; + padding: 12px; + visibility: hidden; + `, + + closeIcon: css` + display: inline-block; + padding: 12px; + cursor: pointer; + `, + + body: css` + border-top-left-radius: 8px; + border-top-right-radius: 8px; + max-height: calc(100% - var(--nb-mobile-page-header-height)); + overflow-y: auto; + overflow-x: hidden; + // background-color: ${token.colorBgLayout}; + + .popover-subform-container { + min-width: initial; + max-width: initial; + max-height: initial; + overflow: hidden; + .ant-card { + border-radius: 0; + } + } + `, + + footer: css` + padding: 8px var(--nb-mobile-page-tabs-content-padding); + display: flex; + align-items: center; + justify-content: flex-end; + position: sticky; + bottom: 0; + left: 0; + right: 0; + z-index: 1000; + border-top: 1px solid ${token.colorSplit}; + background-color: ${token.colorBgLayout}; + `, + }; +}); diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/InternalPopoverNester.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/InternalPopoverNester.tsx new file mode 100644 index 0000000000..3ce77d41d9 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/InternalPopoverNester.tsx @@ -0,0 +1,88 @@ +/** + * 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 { useField } from '@formily/react'; +import { ConfigProvider } from 'antd'; +import { Popup } from 'antd-mobile'; +import { CloseOutline } from 'antd-mobile-icons'; +import React, { useCallback, useMemo } from 'react'; +import { BasicZIndexProvider, MIN_Z_INDEX_INCREMENT, useBasicZIndex } from './BasicZIndexProvider'; +import { usePopupContainer } from './FilterAction'; +import { useInternalPopoverNesterUsedInMobileStyle } from './InternalPopoverNester.style'; + +const Container = (props) => { + const { onOpenChange } = props; + const { visiblePopup, popupContainerRef } = usePopupContainer(props.open); + const { styles } = useInternalPopoverNesterUsedInMobileStyle(); + const field = useField(); + const { basicZIndex } = useBasicZIndex(); + + const newZIndex = basicZIndex + MIN_Z_INDEX_INCREMENT; + const title = field.title || ''; + + const zIndexStyle = useMemo(() => { + return { + zIndex: newZIndex, + }; + }, [newZIndex]); + + const closePopup = useCallback(() => { + onOpenChange(false); + }, [onOpenChange]); + + const openPopup = useCallback(() => { + onOpenChange(true); + }, [onOpenChange]); + + const theme = useMemo(() => { + return { + token: { + zIndexPopupBase: newZIndex, + }, + }; + }, [newZIndex]); + + return ( + + +
{props.children}
+ popupContainerRef.current as HTMLElement} + bodyClassName={styles.body} + bodyStyle={zIndexStyle} + maskStyle={zIndexStyle} + showCloseButton + closeOnSwipe + > +
+ {/* used to make the title center */} + + + + {title} + + + +
+ {props.content} +
+
+
+
+ ); +}; + +export const InternalPopoverNesterUsedInMobile: React.FC<{ OriginComponent: React.FC }> = (props) => { + const { OriginComponent } = props; + + return ; +}; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/ResetSchemaOptionsProvider.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/ResetSchemaOptionsProvider.tsx new file mode 100644 index 0000000000..1e02970ee5 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/ResetSchemaOptionsProvider.tsx @@ -0,0 +1,25 @@ +/** + * 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 { SchemaComponentOptions, useSchemaOptionsContext } from '@nocobase/client'; +import React, { FC, useMemo } from 'react'; +import { useGridCardBlockDecoratorProps } from './useGridCardBlockDecoratorProps'; + +/* 使用移动端专属的 scope 覆盖桌面端的 scope,用于在移动端适配桌面端区块 */ +export const ResetSchemaOptionsProvider: FC = (props) => { + const { scope: desktopScopes } = useSchemaOptionsContext(); + const scopes = useMemo( + () => ({ + useGridCardBlockDecoratorProps: (props) => + useGridCardBlockDecoratorProps(props, desktopScopes?.useGridCardBlockDecoratorProps), + }), + [desktopScopes?.useGridCardBlockDecoratorProps], + ); + return {props.children}; +}; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/mobile-action-page/MobileActionPage.style.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.style.ts similarity index 68% rename from packages/plugins/@nocobase/plugin-mobile/src/client/pages/mobile-action-page/MobileActionPage.style.ts rename to packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.style.ts index 96d9ae0d8b..79ed906710 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/mobile-action-page/MobileActionPage.style.ts +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.style.ts @@ -23,5 +23,19 @@ export const useMobileActionPageStyle = createStyles(({ css, token }: any) => { margin: 20px; } `, + + footer: css` + height: var(--nb-mobile-page-header-height); + padding-right: var(--nb-mobile-page-tabs-content-padding); + display: flex; + align-items: center; + justify-content: flex-end; + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: white; + z-index: 1000; + `, }; }); diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.tsx new file mode 100644 index 0000000000..3c3d474642 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.tsx @@ -0,0 +1,147 @@ +/** + * 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, useField, useFieldSchema } from '@formily/react'; +import { + BackButtonUsedInSubPage, + SchemaComponent, + SchemaInitializer, + TabsContextProvider, + useActionContext, + useApp, + useTabsContext, +} from '@nocobase/client'; +import _ from 'lodash'; +import React, { useEffect, useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { usePluginTranslation } from '../../locale'; +import { BasicZIndexProvider, MIN_Z_INDEX_INCREMENT, useBasicZIndex } from '../BasicZIndexProvider'; +import { useMobileActionPageStyle } from './MobileActionPage.style'; +import { MobileTabsForMobileActionPage } from './MobileTabsForMobileActionPage'; + +const components = { Tabs: MobileTabsForMobileActionPage }; + +/** + * 把 popup:common:addBlock 替换为移动端专属的值。当退出子页面时,再换回来。 + * + * 之所以要把这个过程放到子页面组件这里,是因为 dataBlocks 的 useChildren 必须要在子页面的上下文中运行。 + * + * @param supportsDataBlocks 支持在子页面中使用的数据区块 name + */ +const useMobileBlockInitializersInSubpage = ( + supportsDataBlocks = ['details', 'editForm', 'createForm', 'table', 'gridCard'], +) => { + const app = useApp(); + const [originalInitializers] = useState(() => + app.schemaInitializerManager.get('popup:common:addBlock'), + ); + const { t } = usePluginTranslation(); + const { visible } = useActionContext(); + + const dataBlocks = originalInitializers.options.items.find((item) => item.name === 'dataBlocks'); + const dataBlocksChildren = [...dataBlocks.useChildren(), ...dataBlocks.children]; + + const [newInitializers] = useState(() => { + const options = _.cloneDeep(originalInitializers.options); + options.items = options.items.filter((item) => { + if (item.name === 'dataBlocks') { + item.title = t('Desktop data blocks'); + item.children = dataBlocksChildren.filter((child) => { + return supportsDataBlocks.includes(child.name); + }); + item.useChildren = () => []; + return true; + } + + if (item.name === 'otherBlocks') { + item.title = t('Other desktop blocks'); + } + + return item.name !== 'filterBlocks'; + }); + + return new SchemaInitializer(options); + }); + + useEffect(() => { + return () => { + app.schemaInitializerManager.add(originalInitializers); + }; + }, [app, originalInitializers]); + + if (visible) { + // 把 PC 端子页面的 Add block 按钮换成移动端的。在退出移动端时,再换回来 + app.schemaInitializerManager.add(newInitializers); + } +}; + +/** + * 在移动端通过 Action 按钮打开的页面 + * @returns + */ +export const MobileActionPage = ({ level, footerNodeName }) => { + useMobileBlockInitializersInSubpage(); + + const field = useField(); + const fieldSchema = useFieldSchema(); + const ctx = useActionContext(); + const { styles } = useMobileActionPageStyle(); + const tabContext = useTabsContext(); + const containerDOM = useMemo(() => document.querySelector('.nb-mobile-subpages-slot'), []); + const { basicZIndex } = useBasicZIndex(); + + // in nested popups, basicZIndex is an accumulated value to ensure that + // the z-index of the current level is always higher than the previous level + const newZIndex = basicZIndex + MIN_Z_INDEX_INCREMENT + (level || 1); + + const footerSchema = fieldSchema.reduceProperties((buf, s) => { + if (s['x-component'] === footerNodeName) { + return s; + } + return buf; + }); + + const zIndexStyle = useMemo(() => { + return { + zIndex: newZIndex, + }; + }, [newZIndex]); + + if (!ctx.visible) { + return null; + } + + const actionPageNode = ( + +
+ } tabBarGutter={48}> + + + {footerSchema && ( +
+ { + return s['x-component'] === footerNodeName; + }} + /> +
+ )} +
+
+ ); + + if (containerDOM) { + return createPortal(actionPageNode, containerDOM); + } + + return actionPageNode; +}; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/mobile-action-page/MobileTabsForMobileActionPage.style.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileTabsForMobileActionPage.style.ts similarity index 100% rename from packages/plugins/@nocobase/plugin-mobile/src/client/pages/mobile-action-page/MobileTabsForMobileActionPage.style.ts rename to packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileTabsForMobileActionPage.style.ts diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/mobile-action-page/MobileTabsForMobileActionPage.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileTabsForMobileActionPage.tsx similarity index 77% rename from packages/plugins/@nocobase/plugin-mobile/src/client/pages/mobile-action-page/MobileTabsForMobileActionPage.tsx rename to packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileTabsForMobileActionPage.tsx index b65e0f9044..16c601aa82 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/mobile-action-page/MobileTabsForMobileActionPage.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileTabsForMobileActionPage.tsx @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; +import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; import { css, DndContext, @@ -23,22 +23,35 @@ import { import { Tabs } from 'antd-mobile'; import { LeftOutline } from 'antd-mobile-icons'; import classNames from 'classnames'; -import React, { useMemo, useRef } from 'react'; -import { MobilePageHeader } from '../dynamic-page'; -import { MobilePageContentContainer } from '../dynamic-page/content/MobilePageContentContainer'; -import { useStyles } from '../dynamic-page/header/tabs'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { MobilePageHeader } from '../../pages/dynamic-page'; +import { MobilePageContentContainer } from '../../pages/dynamic-page/content/MobilePageContentContainer'; +import { useStyles } from '../../pages/dynamic-page/header/tabs'; import { useMobileTabsForMobileActionPageStyle } from './MobileTabsForMobileActionPage.style'; export const MobileTabsForMobileActionPage: any = observer( (props) => { const fieldSchema = useFieldSchema(); const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']); - const { activeKey, onChange } = useTabsContext() || {}; + const { activeKey: _activeKey, onChange: _onChange } = useTabsContext() || {}; + const [activeKey, setActiveKey] = useState(_activeKey); const { styles } = useStyles(); const { styles: mobileTabsForMobileActionPageStyle } = useMobileTabsForMobileActionPageStyle(); const { goBack } = useBackButton(); const keyToTabRef = useRef({}); + const onChange = useCallback( + (key) => { + setActiveKey(key); + _onChange?.(key); + }, + [_onChange], + ); + + useEffect(() => { + setActiveKey(_activeKey); + }, [_activeKey]); + const items = useMemo(() => { const result = fieldSchema.mapProperties((schema, key) => { keyToTabRef.current[key] = ; @@ -50,6 +63,7 @@ export const MobileTabsForMobileActionPage: any = observer( const tabContent = useMemo(() => { const list = fieldSchema.mapProperties((schema, key) => { + schema = hideDivider(schema); return { key, node: , @@ -148,3 +162,17 @@ MobileTabsForMobileActionPage.TabPane = observer( ); MobileTabsForMobileActionPage.Designer = TabsOfPC.Designer; + +// 隐藏 Grid 组件的左右 divider,因为移动端不需要在一行中并列展示两个区块 +function hideDivider(tabPaneSchema: Schema) { + tabPaneSchema?.mapProperties((schema) => { + if (schema['x-component'] === 'Grid') { + schema['x-component-props'] = { + ...schema['x-component-props'], + showDivider: false, + }; + } + }); + + return tabPaneSchema; +} diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/useGridCardBlockDecoratorProps.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/useGridCardBlockDecoratorProps.tsx new file mode 100644 index 0000000000..d4ad5cec71 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/useGridCardBlockDecoratorProps.tsx @@ -0,0 +1,27 @@ +/** + * 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. + */ + +/** + * 用于设置 Grid Card 区块的 props,使其样式适配移动端; + * @param oldUseGridCardBlockDecoratorProps + */ +export const useGridCardBlockDecoratorProps = (props, useGridCardBlockDecoratorPropsOfDesktop: any) => { + const oldProps = useGridCardBlockDecoratorPropsOfDesktop(props); + + return { + ...oldProps, + // 在移动端中,无论是什么屏幕尺寸,都只显示一列 + columnCount: { + lg: 1, + md: 1, + xs: 1, + xxl: 1, + }, + }; +}; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/index.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/index.tsx index 4f2afadfdb..58520ce162 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/index.tsx @@ -9,11 +9,11 @@ import { PagePopups, Plugin, RouterManager, createRouterManager } from '@nocobase/client'; import React from 'react'; -import { Outlet } from 'react-router-dom'; - // @ts-ignore import { name } from '../../package.json'; +import { Outlet } from 'react-router-dom'; + import { generatePluginTranslationTemplate } from './locale'; import { Mobile } from './mobile'; import { @@ -184,6 +184,15 @@ export class PluginMobileClient extends Plugin { }, }); + // 跳转到主应用的页面 + this.mobileRouter.add('admin', { + path: `/admin/*`, + Component: () => { + window.location.replace(window.location.href.replace(this.mobilePath, '')); + return null; + }, + }); + this.mobileRouter.add('mobile.schema', { element: , }); diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/MobileProviders.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/MobileProviders.tsx index 30bb49ddfe..3c833edc39 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/MobileProviders.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/MobileProviders.tsx @@ -22,6 +22,7 @@ export const MobileProviders: FC = ({ children, skipLogin useEffect(() => { document.body.style.setProperty('--nb-mobile-page-tabs-content-padding', '12px'); + document.body.style.setProperty('--nb-mobile-page-header-height', '50px'); }, []); return ( diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/context/MobileRoutes.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/context/MobileRoutes.tsx index 5dc7c5615b..508cce4cb0 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/context/MobileRoutes.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/context/MobileRoutes.tsx @@ -7,10 +7,10 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Spin } from 'antd'; -import { useLocation } from 'react-router-dom'; -import React, { createContext, useContext, useEffect, useMemo } from 'react'; import { APIClient, useAPIClient, useRequest } from '@nocobase/client'; +import { Spin } from 'antd'; +import React, { createContext, useContext, useEffect, useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; import type { IResource } from '@nocobase/sdk'; @@ -27,6 +27,7 @@ export interface MobileRouteItem { children?: MobileRouteItem[]; } +export const MobileRoutesContext = createContext(null); export interface MobileRoutesContextValue { routeList?: MobileRouteItem[]; @@ -37,8 +38,6 @@ export interface MobileRoutesContextValue { activeTabItem?: MobileRouteItem; api: APIClient; } - -export const MobileRoutesContext = createContext(null); MobileRoutesContext.displayName = 'MobileRoutesContext'; export const useMobileRoutes = () => { @@ -97,7 +96,9 @@ export const MobileRoutesProvider = ({ children }) => { data, runAsync: refresh, loading, - } = useRequest<{ data: MobileRouteItem[] }>(() => resource.list({ tree: true, sort: 'sort' }).then((res) => res.data)); + } = useRequest<{ data: MobileRouteItem[] }>(() => + resource.list({ tree: true, sort: 'sort' }).then((res) => res.data), + ); const routeList = useMemo(() => data?.data || [], [data]); const { activeTabBarItem, activeTabItem } = useActiveTabBar(routeList); diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/Mobile.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/Mobile.tsx index f7a9cf914d..ac14ee351a 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/Mobile.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/Mobile.tsx @@ -7,21 +7,38 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Action, AntdAppProvider, GlobalThemeProvider, OpenModeProvider, usePlugin } from '@nocobase/client'; +import { + Action, + AntdAppProvider, + AssociationFieldModeProvider, + BlockTemplateProvider, + GlobalThemeProvider, + OpenModeProvider, + usePlugin, +} from '@nocobase/client'; import React from 'react'; import { isDesktop } from 'react-device-detect'; +import _ from 'lodash'; +import { ActionDrawerUsedInMobile, useToAdaptActionDrawerToMobile } from '../adaptor-of-desktop/ActionDrawer'; +import { BasicZIndexProvider } from '../adaptor-of-desktop/BasicZIndexProvider'; +import { useToAdaptFilterActionToMobile } from '../adaptor-of-desktop/FilterAction'; +import { InternalPopoverNesterUsedInMobile } from '../adaptor-of-desktop/InternalPopoverNester'; +import { MobileActionPage } from '../adaptor-of-desktop/mobile-action-page/MobileActionPage'; +import { ResetSchemaOptionsProvider } from '../adaptor-of-desktop/ResetSchemaOptionsProvider'; import { PageBackgroundColor } from '../constants'; import { DesktopMode } from '../desktop-mode/DesktopMode'; import { PluginMobileClient } from '../index'; -import { MobileActionPage } from '../pages/mobile-action-page/MobileActionPage'; import { MobileAppProvider } from './MobileAppContext'; import { useStyles } from './styles'; export const Mobile = () => { + useToAdaptFilterActionToMobile(); + useToAdaptActionDrawerToMobile(); + + const { styles } = useStyles(); const mobilePlugin = usePlugin(PluginMobileClient); const MobileRouter = mobilePlugin.getRouterComponent(); - const { styles } = useStyles(); // 设置的移动端 meta React.useEffect(() => { if (!isDesktop) { @@ -44,36 +61,51 @@ export const Mobile = () => { }, []); const DesktopComponent = mobilePlugin.desktopMode === false ? React.Fragment : DesktopMode; + const modeToComponent = React.useMemo(() => { + return { + PopoverNester: _.memoize((OriginComponent) => (props) => ( + + )), + }; + }, []); + return ( {/* 目前移动端由于和客户端的主题对不上,所以先使用 `GlobalThemeProvider` 和 `AntdAppProvider` 进行重置为默认主题 */} -
- - - + + + + - + + + {/* the z-index of all popups and subpages will be based on this value */} + + + + + - - - -
+ + + +
); }; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/styles.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/styles.ts index 3a19bf1917..1280cb028b 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/styles.ts +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/styles.ts @@ -19,6 +19,19 @@ export const useStyles = createStyles(({ token, css }) => { .ant-table-thead button[aria-label*='schema-initializer-TableV2-table:configureColumns'] > .ant-btn-icon { margin: 0px; } + + // reset Select record popup + .ant-table-thead + button[aria-label*='schema-initializer-TableV2.Selector-table:configureColumns'] + > span:last-child { + display: none !important; + } + .ant-table-thead + button[aria-label*='schema-initializer-TableV2.Selector-table:configureColumns'] + > .ant-btn-icon { + margin: 0px; + } + .ant-pagination .ant-pagination-total-text { display: none; } diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/content/initializer.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/content/initializer.tsx index b6998b35a6..de22d591e8 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/content/initializer.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/content/initializer.tsx @@ -20,7 +20,7 @@ export const mobileAddBlockInitializer = new SchemaInitializer({ items: [ { name: 'dataBlocks', - title: '{{t("Data blocks")}}', + title: '{{t("Desktop data blocks")}}', type: 'itemGroup', children: [ { @@ -53,7 +53,7 @@ export const mobileAddBlockInitializer = new SchemaInitializer({ { name: 'otherBlocks', type: 'itemGroup', - title: '{{t("Other blocks")}}', + title: '{{t("Other desktop blocks")}}', children: [ { name: 'markdown', diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/mobile-action-page/MobileActionPage.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/pages/mobile-action-page/MobileActionPage.tsx deleted file mode 100644 index 0e2d927ff1..0000000000 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/mobile-action-page/MobileActionPage.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** - * 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 { useFieldSchema } from '@formily/react'; -import { - BackButtonUsedInSubPage, - SchemaComponent, - TabsContextProvider, - useActionContext, - useTabsContext, -} from '@nocobase/client'; -import React, { useMemo } from 'react'; -import { createPortal } from 'react-dom'; -import { useMobileActionPageStyle } from './MobileActionPage.style'; -import { MobileTabsForMobileActionPage } from './MobileTabsForMobileActionPage'; - -const components = { Tabs: MobileTabsForMobileActionPage }; - -/** - * 在移动端通过 Action 按钮打开的页面 - * @returns - */ -export const MobileActionPage = ({ level }) => { - const filedSchema = useFieldSchema(); - const ctx = useActionContext(); - const { styles } = useMobileActionPageStyle(); - const tabContext = useTabsContext(); - const containerDOM = useMemo(() => document.querySelector('.nb-mobile-subpages-slot'), []); - - const style = useMemo(() => { - return { - // 10 为基数,是为了要确保能大于 Table 中的悬浮行的 z-index - zIndex: 10 + level, - }; - }, [level]); - - if (!ctx.visible) { - return null; - } - - const actionPageNode = ( -
- } tabBarGutter={48}> - - -
- ); - - if (containerDOM) { - return createPortal(actionPageNode, containerDOM); - } - - return actionPageNode; -}; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-mobile/src/locale/en-US.json index ccdd3fcfcf..f99605a80b 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/locale/en-US.json +++ b/packages/plugins/@nocobase/plugin-mobile/src/locale/en-US.json @@ -18,5 +18,7 @@ "Add tab": "Add tab", "Mobile": "Mobile", "Title field is required": "Title field is required", - "Icon field is required": "Icon field is required" + "Icon field is required": "Icon field is required", + "Desktop data blocks": "Desktop data blocks", + "Other desktop blocks": "Other desktop blocks" } diff --git a/packages/plugins/@nocobase/plugin-mobile/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-mobile/src/locale/zh-CN.json index 6569da685e..58c7f52b16 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-mobile/src/locale/zh-CN.json @@ -19,5 +19,7 @@ "Add tab": "添加标签页", "Mobile": "移动端", "Title field is required": "标题必填", - "Icon field is required": "图标必填" + "Icon field is required": "图标必填", + "Desktop data blocks": "桌面端数据区块", + "Other desktop blocks": "其他桌面端区块" } diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowManualProvider.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowManualProvider.tsx new file mode 100644 index 0000000000..e49f458f03 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowManualProvider.tsx @@ -0,0 +1,45 @@ +/** + * 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 { ExtendCollectionsProvider, storePopupContext } from '@nocobase/client'; +import React, { FC } from 'react'; +import { getWorkflowTodoViewActionSchema, nodeCollection, todoCollection, workflowCollection } from './WorkflowTodo'; + +const collections = [nodeCollection, workflowCollection, todoCollection]; + +/** + * 1. 扩展几个工作流相关的 collection,防止在区块中因找不到 collection 而报错; + * @param props + * @returns + */ +export const WorkflowManualProvider: FC = (props) => { + return {props.children}; +}; + +/** + * 2. 将区块相关的按钮 Schema 缓存起来,这样就可以在弹窗中获取到 Schema,进而实现“弹窗 URL”的功能; + */ +function cacheSchema(collectionNameList: string[]) { + collectionNameList.forEach((collectionName) => { + const defaultOpenMode = isMobile() ? 'drawer' : 'page'; + const workflowTodoViewActionSchema = getWorkflowTodoViewActionSchema({ defaultOpenMode, collectionName }); + + storePopupContext(workflowTodoViewActionSchema['x-uid'], { + schema: workflowTodoViewActionSchema, + ...workflowTodoViewActionSchema['x-action-context'], + notBackToPreviousPath: true, + }); + }); +} + +cacheSchema(Object.values(collections).map((collection) => collection.name)); + +function isMobile() { + return window.location.pathname.startsWith('/m'); +} diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx index 02609fd8a7..22ba76c1f0 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx @@ -12,7 +12,14 @@ import { Button, Space, Spin, Tag } from 'antd'; import dayjs from 'dayjs'; import React, { createContext, useContext, useEffect, useState } from 'react'; -import { css, useCompile, usePlugin } from '@nocobase/client'; +import { + css, + useCollection, + useCollectionRecordData, + useCompile, + useOpenModeContext, + usePlugin, +} from '@nocobase/client'; import { SchemaComponent, @@ -22,25 +29,23 @@ import { useActionContext, useCurrentUserContext, useFormBlockContext, - useRecord, useTableBlockContext, - ExtendCollectionsProvider, } from '@nocobase/client'; import WorkflowPlugin, { + DetailsBlockProvider, FlowContext, JobStatusOptions, JobStatusOptionsMap, linkNodes, useAvailableUpstreams, useFlowContext, - DetailsBlockProvider, } from '@nocobase/plugin-workflow/client'; import { NAMESPACE, useLang } from '../locale'; import { FormBlockProvider } from './instruction/FormBlockProvider'; import { ManualFormType, manualFormTypes } from './instruction/SchemaConfig'; -const nodeCollection = { +export const nodeCollection = { title: `{{t("Task", { ns: "${NAMESPACE}" })}}`, name: 'flow_nodes', fields: [ @@ -81,7 +86,7 @@ const nodeCollection = { ], }; -const workflowCollection = { +export const workflowCollection = { title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`, name: 'workflows', fields: [ @@ -99,7 +104,7 @@ const workflowCollection = { ], }; -const todoCollection = { +export const todoCollection = { title: `{{t("Workflow todos", { ns: "${NAMESPACE}" })}}`, name: 'users_jobs', fields: [ @@ -219,15 +224,18 @@ const UserColumn = observer( ); function UserJobStatusColumn(props) { - const record = useRecord(); + const recordData = useCollectionRecordData(); const labelUnprocessed = useLang('Unprocessed'); - if (record.execution.status && !record.status) { + if (recordData?.execution?.status && !recordData?.status) { return {labelUnprocessed}; } return props.children; } export const WorkflowTodo: React.FC & { Drawer: React.FC; Decorator: React.FC } = () => { + const { defaultOpenMode } = useOpenModeContext(); + const collection = useCollection(); + return ( (null); const [node, setNode] = useState(null); @@ -558,13 +584,13 @@ function FlowContextProvider(props) { function useFormBlockProps() { const { userJob, execution } = useFlowContext(); - const record = useRecord(); + const recordData = useCollectionRecordData(); const { data: user } = useCurrentUserContext(); const { form } = useFormBlockContext(); const pattern = execution.status || userJob.status - ? record + ? recordData ? 'readPretty' : 'disabled' : user?.data?.id !== userJob.userId @@ -585,7 +611,7 @@ function useDetailsBlockProps() { function FooterStatus() { const compile = useCompile(); - const { status, updatedAt } = useRecord(); + const { status, updatedAt } = useCollectionRecordData() || {}; const statusOption = JobStatusOptionsMap[status]; return status ? ( @@ -605,7 +631,7 @@ function FooterStatus() { function Drawer() { const ctx = useContext(SchemaComponentContext); - const { id, node, workflow, status } = useRecord(); + const { id, node, workflow, status } = useCollectionRecordData() || {}; return ( @@ -617,11 +643,11 @@ function Drawer() { schema={{ type: 'void', name: `drawer-${id}-${status}`, - 'x-component': 'Action.Drawer', + 'x-component': 'Action.Container', 'x-component-props': { className: 'nb-action-popup', }, - title: `${workflow.title} - ${node.title ?? `#${node.id}`}`, + title: `${workflow?.title} - ${node?.title ?? `#${node?.id}`}`, properties: { tabs: { type: 'void', @@ -629,7 +655,7 @@ function Drawer() { }, footer: { type: 'void', - 'x-component': 'Action.Drawer.Footer', + 'x-component': 'Action.Container.Footer', properties: { content: { type: 'void', @@ -662,11 +688,9 @@ function Decorator({ params = {}, children }) { }; return ( - - - {children} - - + + {children} + ); } diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodoBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodoBlockInitializer.tsx index f257f14160..380cbe7905 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodoBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodoBlockInitializer.tsx @@ -25,7 +25,6 @@ export const WorkflowTodoBlockInitializer: FC = () => { 'x-decorator': 'WorkflowTodo.Decorator', 'x-decorator-props': {}, 'x-component': 'CardItem', - // 'x-designer': 'TableBlockDesigner', 'x-toolbar': 'BlockSchemaToolbar', 'x-settings': 'blockSettings:table', properties: { diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/datablocks.test.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/datablocks.test.ts index d7eaa7650b..f24c4b9539 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/datablocks.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/datablocks.test.ts @@ -313,7 +313,7 @@ test.describe('field data', () => { // await expect(page.getByText('8')).toBeAttached(); await expect( page - .getByLabel(`block-item-CardItem-users_jobs-workflow-todo-${preAggregateNodeTitle}`) + .getByLabel(`block-item-CardItem-users_jobs-${preAggregateNodeTitle}`) .locator('.ant-card-body') .getByText('8'), ).toBeAttached(); diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/index.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/index.ts index 05cd7d105c..4e5b9c8b9c 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/index.ts +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/index.ts @@ -13,6 +13,7 @@ import WorkflowPlugin from '@nocobase/plugin-workflow/client'; import Manual from './instruction'; import { NAMESPACE } from '../locale'; +import { WorkflowManualProvider } from './WorkflowManualProvider'; import { WorkflowTodo } from './WorkflowTodo'; import { WorkflowTodoBlockInitializer } from './WorkflowTodoBlockInitializer'; import { @@ -49,6 +50,14 @@ export default class extends Plugin { Component: 'WorkflowTodoBlockInitializer', icon: 'CheckSquareOutlined', }); + + this.app.schemaInitializerManager.addItem('mobile:addBlock', 'otherBlocks.workflowTodos', { + title: `{{t("Workflow todos", { ns: "${NAMESPACE}" })}}`, + Component: 'WorkflowTodoBlockInitializer', + icon: 'CheckSquareOutlined', + }); + + this.app.addProvider(WorkflowManualProvider); } addComponents() { diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockProvider.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockProvider.tsx index 9d421872e9..670fa87b10 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockProvider.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockProvider.tsx @@ -9,7 +9,6 @@ import { createForm } from '@formily/core'; import { useField, useFieldSchema } from '@formily/react'; -import { theme } from 'antd'; import { BlockRequestContext_deprecated, CollectionManagerProvider, @@ -22,14 +21,14 @@ import { useAPIClient, useAssociationNames, useBlockRequestContext, + useCollectionRecordData, useDataSourceHeaders, - useDesignable, - useRecord, } from '@nocobase/client'; +import { theme } from 'antd'; import React, { useMemo, useRef } from 'react'; export function FormBlockProvider(props) { - const userJob = useRecord(); + const userJob = useCollectionRecordData(); const fieldSchema = useFieldSchema(); const field = useField(); const formBlockRef = useRef(null); @@ -40,8 +39,6 @@ export function FormBlockProvider(props) { const [formKey] = Object.keys(fieldSchema.toJSON().properties ?? {}); const values = userJob?.result?.[formKey]; - const { findComponent } = useDesignable(); - const form = useMemo( () => createForm({ @@ -81,7 +78,7 @@ export function FormBlockProvider(props) { }; }, [field, form, params, service, updateAssociationValues]); - return !userJob.status || values ? ( + return !userJob?.status || values ? ( diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/custom.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/custom.tsx index 44d43f1c71..dfeb647a98 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/custom.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/custom.tsx @@ -26,8 +26,8 @@ import { SchemaInitializerItems, gridRowColWrap, useCollectionManager_deprecated, + useCollectionRecordData, useCollection_deprecated, - useRecord, useSchemaInitializer, useSchemaInitializerItem, } from '@nocobase/client'; @@ -40,7 +40,7 @@ import { findSchema } from '../utils'; function CustomFormBlockProvider(props) { const [fields, setCollectionFields] = useState(props.collection?.fields ?? []); - const userJob = useRecord(); + const userJob = useCollectionRecordData(); const field = useField(); const fieldSchema = useFieldSchema(); const [formKey] = Object.keys(fieldSchema.toJSON().properties ?? {}); @@ -54,7 +54,7 @@ function CustomFormBlockProvider(props) { [values], ); - return !userJob.status || values ? ( + return !userJob?.status || values ? (