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 <chenlinxh@gmail.com>
Co-authored-by: katherinehhh <katherine_15995@163.com>
This commit is contained in:
Zeke Zhang 2024-08-07 14:25:40 +08:00 committed by GitHub
parent 64e53558df
commit a429b7a4b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
109 changed files with 1630 additions and 449 deletions

View File

@ -2,9 +2,7 @@
"version": "1.3.0-alpha",
"npmClient": "yarn",
"useWorkspaces": true,
"npmClientArgs": [
"--ignore-engines"
],
"npmClientArgs": ["--ignore-engines"],
"command": {
"version": {
"forcePublish": true,

View File

@ -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, ''));
}
}

View File

@ -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();

View File

@ -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 (
<App
className={className}
style={{
height: '100%',
...style,
}}
>
<AppInner>{children}</AppInner>

View File

@ -44,7 +44,7 @@ const commonOptions = {
showAssociationFields: true,
onlyCurrentDataSource: true,
hideSearch: true,
componentType: 'FormItem',
componentType: `FormItem`,
currentText: t('Current collection'),
otherText: t('Other collections'),
},

View File

@ -29,10 +29,11 @@ export const SchemaSettingsActionLinkItem: FC<SchemaSettingsActionLinkItemProps>
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<SchemaSettingsActionLinkItemProps>
},
}}
onSubmit={({ url, params, openInNewWindow }) => {
const componentProps = fieldSchema['x-component-props'] || {};
componentProps.url = url;
componentProps.params = params;
componentProps.openInNewWindow = openInNewWindow;

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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,
};

View File

@ -49,7 +49,7 @@ export const GridCardBlockInitializer = ({
<DataBlockInitializer
{...itemConfig}
icon={<OrderedListOutlined />}
componentType={'GridCard'}
componentType={`GridCard`}
onCreateBlockSchema={async (options) => {
if (createBlockSchema) {
return createBlockSchema(options);

View File

@ -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,
};

View File

@ -49,7 +49,7 @@ export const ListBlockInitializer = ({
<DataBlockInitializer
{...itemConfig}
icon={<OrderedListOutlined />}
componentType={'List'}
componentType={`List`}
onCreateBlockSchema={async (options) => {
if (createBlockSchema) {
return createBlockSchema(options);

View File

@ -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,
};

View File

@ -44,7 +44,7 @@ export const TableBlockInitializer = ({
<DataBlockInitializer
{...itemConfig}
icon={<TableOutlined />}
componentType={'Table'}
componentType={`Table`}
onCreateBlockSchema={async (options) => {
if (createBlockSchema) {
return createBlockSchema(options);

View File

@ -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,
};

View File

@ -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={<TableOutlined />}
componentType={'FilterCollapse'}
componentType={`FilterCollapse`}
onCreateBlockSchema={async ({ item }) => {
const schema = createCollapseBlockSchema({
dataSource: item.dataSource,

View File

@ -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,
};

View File

@ -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 (
<FilterBlockInitializer
{...itemConfig}
icon={<FormOutlined />}
onlyCurrentDataSource={onlyCurrentDataSource}
componentType={'FilterFormItem'}
componentType={`${componentNamePrefix}FilterFormItem`}
templateWrap={(templateSchema, { item }) => {
const s = createFilterFormBlockSchema({
templateSchema: templateSchema,

View File

@ -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,
};

View File

@ -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);

View File

@ -43,19 +43,19 @@ export const ActionContextProvider: React.FC<ActionContextProps & { value?: Acti
const useBlockServiceInActionButton = () => {
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;
}

View File

@ -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 (
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
<Router location={window.location} navigator={null}>
<CustomRouterContextProvider>
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
);
});

View File

@ -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 (
<SchemaComponentProvider components={{ Form, Action, Input, FormItem }}>
<ActionContextProvider value={{ visible, setVisible }}>
<a onClick={() => setVisible(true)}>Open</a>
<SchemaComponent scope={{ useCloseAction }} schema={schema} />
</ActionContextProvider>
</SchemaComponentProvider>
<Router location={window.location} navigator={null}>
<CustomRouterContextProvider>
<SchemaComponentProvider components={{ Form, Action, Input, FormItem }}>
<ActionContextProvider value={{ visible, setVisible }}>
<a onClick={() => setVisible(true)}>Open</a>
<SchemaComponent scope={{ useCloseAction }} schema={schema} />
</ActionContextProvider>
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
);
});

View File

@ -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 (
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
<Router location={window.location} navigator={null}>
<CustomRouterContextProvider>
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
);
});

View File

@ -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<Record<AssociationFieldMode, React.FC | ((originalCom: React.FC) => 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<AssociationFieldModeProviderProps> = (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 <AssociationFieldModeContext.Provider value={value}>{props.children}</AssociationFieldModeContext.Provider>;
};
export const useAssociationFieldModeContext = () => {
return React.useContext(AssociationFieldModeContext);
};

View File

@ -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 (
<SchemaComponentOptions scope={{ useCreateActionProps }} components={{ CreateRecordAction }}>
{currentMode === 'Picker' && <InternalPicker {...props} />}
{currentMode === 'Nester' && <InternalNester {...props} />}
{currentMode === 'PopoverNester' && <InternalPopoverNester {...props} />}
{currentMode === 'Select' && <AssociationSelect {...props} />}
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
{currentMode === 'FileManager' && <InternalFileManager {...props} />}
{currentMode === 'CascadeSelect' && <InternalCascadeSelect {...props} />}
<Component {...props} />
</SchemaComponentOptions>
);
},

View File

@ -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 = (
<div
ref={ref}
style={{ minWidth: '600px', maxWidth: '800px', maxHeight: '440px', overflow: 'auto' }}
className={InternalPopoverNesterContentCss}
>
<div ref={ref} className={`${InternalPopoverNesterContentCss} popover-subform-container`}>
<InternalNester {...nesterProps} />
</div>
);
@ -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 (
<ActionContextProvider value={{ ...ctx, modalProps }}>
<StablePopover
overlayStyle={{ padding: '0px' }}
<Container
overlayStyle={overlayStyle}
content={content}
trigger="click"
placement="topLeft"
open={visible}
onOpenChange={(open) => setVisible(open)}
onOpenChange={handleOpenChange}
title={t(options?.uiSchema?.rawTitle)}
>
<span style={{ cursor: 'pointer', display: 'flex' }}>
@ -82,7 +93,7 @@ export const InternalPopoverNester = observer(
</div>
<EditOutlined style={{ display: 'inline-flex', margin: '5px' }} />
</span>
</StablePopover>
</Container>
{visible && (
<div
role="button"

View File

@ -25,6 +25,7 @@ import { getPath } from '../../../variables/utils/getPath';
import { getVariableName } from '../../../variables/utils/getVariableName';
import { isVariable } from '../../../variables/utils/isVariable';
import { useDesignable } from '../../hooks';
import { AssociationFieldMode } from './AssociationFieldModeProvider';
import { AssociationFieldContext } from './context';
export const useInsertSchema = (component) => {
@ -51,7 +52,7 @@ export function useAssociationFieldContext<F extends GeneralField>() {
return useContext(AssociationFieldContext) as {
options: any;
field: F;
currentMode: string;
currentMode: AssociationFieldMode;
allowMultiple?: boolean;
allowDissociate?: boolean;
};

View File

@ -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;

View File

@ -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 (
<GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettingsBlockTitleItem />
<SchemaSettingsTemplate componentName={'FilterCollapse'} collectionName={name} resourceName={defaultResource} />
<SchemaSettingsTemplate
componentName={`${componentNamePrefix}FilterCollapse`}
collectionName={name}
resourceName={defaultResource}
/>
<SchemaSettingsConnectDataBlocks type={FilterBlockType.COLLAPSE} emptyDescription={t('No blocks to connect')} />
<SchemaSettingsDivider />
<SchemaSettingsRemove

View File

@ -99,6 +99,8 @@ const InternalAssociationSelect = connect(
mapReadPretty(ReadPretty),
);
InternalAssociationSelect.displayName = 'InternalAssociationSelect';
interface AssociationSelectInterface {
(props: any): React.ReactElement;
Designer: React.FC;

View File

@ -30,6 +30,17 @@ export type FilterActionProps<T = {}> = 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<Form>(() => props.form || createForm(), []);
// 新版 UISchema1.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 (
<FilterActionContext.Provider value={{ field, fieldSchema, designable, dn }}>
<StablePopover
<Container
destroyTooltipOnHide
placement={'bottomLeft'}
open={visible}
@ -112,8 +123,8 @@ export const FilterAction = withDynamicSchemaProps(
</form>
}
>
<Action {...others} title={field.title} />
</StablePopover>
<Action onClick={() => setVisible(!visible)} {...others} title={field.title} />
</Container>
</FilterActionContext.Provider>
);
}),

View File

@ -62,7 +62,7 @@ export const FilterItem = observer(
return (
// 添加 nc-filter-item 类名是为了帮助编写测试时更容易选中该元素
<div style={style} className="nc-filter-item">
<Space>
<Space wrap>
<Cascader
// @ts-ignore
role="button"
@ -90,14 +90,16 @@ export const FilterItem = observer(
onChange={onOperatorsChange}
placeholder={t('Comparision')}
/>
{!operator?.noValue ? (
<DynamicComponent value={value} schema={schema} collectionField={collectionField} onChange={setValue} />
) : null}
{!props.disabled && (
<a role="button" aria-label="icon-close">
<CloseCircleOutlined onClick={remove} style={removeStyle} />
</a>
)}
<Space>
{!operator?.noValue ? (
<DynamicComponent value={value} schema={schema} collectionField={collectionField} onChange={setValue} />
) : null}
{!props.disabled && (
<a role="button" aria-label="icon-close">
<CloseCircleOutlined onClick={remove} style={removeStyle} />
</a>
)}
</Space>
</Space>
</div>
);

View File

@ -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 (
<SchemaComponentProvider components={{ FilterAction, Filter, Input }} scope={{ options }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
<Router location={window.location} navigator={null}>
<CustomRouterContextProvider>
<SchemaComponentProvider components={{ FilterAction, Filter, Input }} scope={{ options }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
);
};

View File

@ -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,
};

View File

@ -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,
};
},

View File

@ -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<any, any> & { uid?: string };
@ -130,9 +131,10 @@ export const Form: React.FC<FormProps> & { Designer?: any } = observer(
Form.Designer = function Designer() {
const { name, title } = useCollection_deprecated();
const template = useSchemaTemplate();
const { componentNamePrefix } = useBlockTemplateContext();
return (
<GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettingsTemplate componentName={'Form'} collectionName={name} />
<SchemaSettingsTemplate componentName={`${componentNamePrefix}Form`} collectionName={name} />
<SchemaSettingsDivider />
<SchemaSettingsRemove
removeParentsIfNoChildren

View File

@ -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',
@ -60,8 +66,12 @@ const Output = observer(
export default observer(() => {
return (
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
<Router location={window.location} navigator={null}>
<CustomRouterContextProvider>
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
);
});

View File

@ -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 (
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
<Router location={window.location} navigator={null}>
<CustomRouterContextProvider>
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
);
});

View File

@ -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 (
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
<Router location={window.location} navigator={null}>
<CustomRouterContextProvider>
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
);
});

View File

@ -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 (
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
<Router location={window.location} navigator={null}>
<CustomRouterContextProvider>
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
);
});

View File

@ -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 (
<APIClientProvider apiClient={apiClient}>
<SchemaComponentProvider
scope={{ useSubmit, useRefresh }}
components={{ Space, Card, Output, Action, Form, Input, FormItem }}
>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</APIClientProvider>
<Router location={window.location} navigator={null}>
<CustomRouterContextProvider>
<APIClientProvider apiClient={apiClient}>
<SchemaComponentProvider
scope={{ useSubmit, useRefresh }}
components={{ Space, Card, Output, Action, Form, Input, FormItem }}
>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</APIClientProvider>
</CustomRouterContextProvider>
</Router>
);
});

View File

@ -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 (
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
<Router location={window.location} navigator={null}>
<CustomRouterContextProvider>
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
);
});

View File

@ -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 (
<SchemaComponentProvider
scope={{ useSubmit, useValues }}
components={{ Card, Output, Action, Form, Input, FormItem }}
>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
<Router location={window.location} navigator={null}>
<CustomRouterContextProvider>
<SchemaComponentProvider
scope={{ useSubmit, useValues }}
components={{ Card, Output, Action, Form, Input, FormItem }}
>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
);
});

View File

@ -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 (
<SchemaComponentProvider components={{ Action, Input, FormItem, Form }} scope={{ useCloseAction }}>
<ActionContextProvider value={{ visible, setVisible }}>
<Button
onClick={() => {
setVisible(true);
}}
>
Edit
</Button>
<SchemaComponent schema={schema} />
</ActionContextProvider>
</SchemaComponentProvider>
<Router location={window.location} navigator={null}>
<CustomRouterContextProvider>
<SchemaComponentProvider components={{ Action, Input, FormItem, Form }} scope={{ useCloseAction }}>
<ActionContextProvider value={{ visible, setVisible }}>
<Button
onClick={() => {
setVisible(true);
}}
>
Edit
</Button>
<SchemaComponent schema={schema} />
</ActionContextProvider>
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
);
});

View File

@ -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 = () => {
});
}}
/>
<SchemaSettingsTemplate componentName={'GridCard'} collectionName={name} resourceName={defaultResource} />
<SchemaSettingsTemplate
componentName={`${componentNamePrefix}GridCard`}
collectionName={name}
resourceName={defaultResource}
/>
<SchemaSettingsDivider />
<SchemaSettingsRemove
removeParentsIfNoChildren

View File

@ -15,7 +15,6 @@ import { useTranslation } from 'react-i18next';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import { useCollection_deprecated, useSortFields } from '../../../collection-manager';
import { SetDataLoadingMode } from '../../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
import { useRecord } from '../../../record-provider';
import {
GeneralSchemaDesigner,
SchemaSettingsDivider,
@ -27,6 +26,7 @@ import { SchemaSettingsBlockTitleItem } from '../../../schema-settings/SchemaSet
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';
@ -42,7 +42,6 @@ export const ListDesigner = () => {
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 (
<GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettingsBlockTitleItem />
@ -187,7 +187,11 @@ export const ListDesigner = () => {
});
}}
/>
<SchemaSettingsTemplate componentName={'List'} collectionName={name} resourceName={defaultResource} />
<SchemaSettingsTemplate
componentName={`${componentNamePrefix}List`}
collectionName={name}
resourceName={defaultResource}
/>
<SchemaSettingsDivider />
<SchemaSettingsRemove
removeParentsIfNoChildren

View File

@ -11,15 +11,13 @@ import { ArrowLeftOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import React, { useCallback, useMemo } from 'react';
import { useToken } from '../../../style';
import { useCurrentPopupContext } from './PagePopups';
import { usePagePopup } from './pagePopupUtils';
import { useActionContext } from '../action/hooks';
export const useBackButton = () => {
const { params } = useCurrentPopupContext();
const { closePopup } = usePagePopup();
const { setVisible } = useActionContext();
const goBack = useCallback(() => {
closePopup(params?.popupuid);
}, [closePopup, params?.popupuid]);
setVisible?.(false);
}, [setVisible]);
return {
goBack,

View File

@ -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';

View File

@ -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<string, PopupContextStorage> = {};
@ -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(

View File

@ -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<any> = (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);

View File

@ -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 = () => {
<SchemaSettingsConnectDataBlocks type={FilterBlockType.TABLE} emptyDescription={t('No blocks to connect')} />
{supportTemplate && <SchemaSettingsDivider />}
{supportTemplate && (
<SchemaSettingsTemplate componentName={'Table'} collectionName={name} resourceName={defaultResource} />
<SchemaSettingsTemplate
componentName={`${componentNamePrefix}Table`}
collectionName={name}
resourceName={defaultResource}
/>
)}
<SchemaSettingsDivider />
<SchemaSettingsRemove

View File

@ -23,6 +23,7 @@ import {
} from '../../../schema-settings';
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
import { useSchemaTemplate } from '../../../schema-templates';
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
import { useDesignable } from '../../hooks';
export const TableVoidDesigner = () => {
@ -48,6 +49,7 @@ export const TableVoidDesigner = () => {
};
});
const template = useSchemaTemplate();
const { componentNamePrefix } = useBlockTemplateContext();
return (
<GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettingsSwitchItem
@ -213,7 +215,7 @@ export const TableVoidDesigner = () => {
}}
/>
<SchemaSettingsDivider />
<SchemaSettingsTemplate componentName={'Table'} collectionName={name} />
<SchemaSettingsTemplate componentName={`${componentNamePrefix}Table`} collectionName={name} />
<SchemaSettingsDivider />
<SchemaSettingsRemove
removeParentsIfNoChildren

View File

@ -10,6 +10,7 @@
import { Schema } from '@formily/react';
import { useCallback, useMemo } from 'react';
import {
useActionAvailable,
useCollection,
useCollectionManager_deprecated,
useCollection_deprecated,
@ -22,7 +23,6 @@ import {
useCreateEditFormBlock,
useCreateFormBlock,
useCreateTableBlock,
useActionAvailable,
} from '../..';
import { CompatibleSchemaInitializer } from '../../application/schema-initializer/CompatibleSchemaInitializer';
import { useCreateDetailsBlock } from '../../modules/blocks/data-blocks/details-multi/DetailsBlockInitializer';
@ -101,7 +101,7 @@ function useRecordBlocks() {
},
onlyCurrentDataSource: true,
hideSearch: true,
componentType: 'ReadPrettyFormItem',
componentType: `ReadPrettyFormItem`,
createBlockSchema,
templateWrap: useCallback(
(templateSchema, { item }) => {
@ -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 });

View File

@ -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")}}',

View File

@ -271,7 +271,7 @@ function FinallyButton({
}),
React.cloneElement(rightButton as React.ReactElement<any, string>, {
loading: false,
style: props?.style,
style: { ...props?.style, justifyContent: 'center' },
}),
]}
menu={menu}

View File

@ -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,
]);
}

View File

@ -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';

View File

@ -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) {

View File

@ -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<any>({});
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 ? (
<BlockTemplateContext.Provider value={{ dn, field, fieldSchema, template }}>
<BlockTemplateProvider {...{ dn, field, fieldSchema, template }}>
<RemoteSchemaComponent noForm uid={template?.uid} onSuccess={onSuccess} />
</BlockTemplateContext.Provider>
</BlockTemplateProvider>
) : (
<CollectionDeletedPlaceholder type="Block template" name={templateId} />
);

View File

@ -0,0 +1,35 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { 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<BlockTemplateProviderProps>({ componentNamePrefix: '' });
export const BlockTemplateProvider: FC<BlockTemplateProviderProps> = (props) => {
return (
<BlockTemplateContext.Provider value={{ ...props, componentNamePrefix: props.componentNamePrefix || '' }}>
{props.children}
</BlockTemplateContext.Provider>
);
};
export const useBlockTemplateContext = () => {
return useContext(BlockTemplateContext);
};

View File

@ -9,4 +9,5 @@
export * from './BlockTemplateDetails';
export * from './BlockTemplatePage';
export * from './BlockTemplateProvider';
export * from './SchemaTemplateManagerProvider';

View File

@ -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';

View File

@ -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();
});

View File

@ -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: {

View File

@ -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: {

View File

@ -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",
},

View File

@ -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,
};

View File

@ -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',
},

View File

@ -45,7 +45,7 @@ export const CalendarBlockInitializer = ({
return (
<DataBlockInitializer
{...itemConfig}
componentType={'Calendar'}
componentType={`Calendar`}
icon={<FormOutlined />}
onCreateBlockSchema={async (options) => {
if (createBlockSchema) {

View File

@ -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,
};
},

View File

@ -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 (
<DataBlockInitializer
{...itemConfig}
componentType={'Calendar'}
componentType={`Calendar`}
icon={<FormOutlined />}
onCreateBlockSchema={async (options) => {
if (createBlockSchema) {

View File

@ -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,
};
},

View File

@ -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 (
<DataBlockInitializer
{...itemConfig}
componentType={'Calendar'}
componentType={`Calendar`}
icon={<FormOutlined />}
onCreateBlockSchema={async (options) => {
if (createBlockSchema) {

View File

@ -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",
},

View File

@ -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,
};

View File

@ -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 (
<DataBlockInitializer
componentType={'Map'}
componentType={`Map`}
icon={<TableOutlined />}
onCreateBlockSchema={async ({ item }) => {
const mapFieldOptions = getCollectionFieldsOptions(item.name, ['point', 'lineString', 'polygon'], {

View File

@ -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',
},

View File

@ -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();

View File

@ -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();

View File

@ -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};
`,
};
});

View File

@ -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 (
<BasicZIndexProvider basicZIndex={newZIndex}>
<ConfigProvider theme={theme}>
<Popup
visible={visiblePopup}
onClose={closePopup}
onMaskClick={closePopup}
getContainer={() => popupContainerRef.current}
bodyClassName={styles.body}
bodyStyle={zIndexStyle}
maskStyle={zIndexStyle}
closeOnSwipe
>
<div className={styles.header}>
{/* used to make the title center */}
<span className={styles.placeholder}>
<CloseOutline />
</span>
<span>{title}</span>
<span className={styles.closeIcon} onClick={closePopup}>
<CloseOutline />
</span>
</div>
<SchemaComponent
schema={fieldSchema}
onlyRenderProperties
filterProperties={(s) => {
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 */}
<div style={{ marginBottom: 1 - marginBlock }}></div>
{footerSchema ? (
<div className={styles.footer}>
<RecursionField
basePath={field.address}
schema={fieldSchema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === props.footerNodeName;
}}
/>
</div>
) : null}
</Popup>
</ConfigProvider>
</BasicZIndexProvider>
);
});
ActionDrawerUsedInMobile.displayName = 'ActionDrawerUsedInMobile';
const originalActionDrawer = Action.Drawer;
/**
* adapt Action.Drawer to mobile
*/
export const useToAdaptActionDrawerToMobile = () => {
Action.Drawer = ActionDrawerUsedInMobile;
useEffect(() => {
return () => {
Action.Drawer = originalActionDrawer;
};
}, []);
};

View File

@ -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 <BasicZIndexContext.Provider value={value}>{props.children}</BasicZIndexContext.Provider>;
};
export const useBasicZIndex = () => {
return React.useContext(BasicZIndexContext);
};
// minimum z-index increment
export const MIN_Z_INDEX_INCREMENT = 10;

View File

@ -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 (
<OriginFilterAction
{...props}
Container={(props) => {
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 (
<ConfigProvider theme={theme}>
{props.children}
<Popup
visible={visiblePopup}
onClose={closePopup}
onMaskClick={closePopup}
getContainer={getContainer}
bodyStyle={bodyStyle}
maskStyle={zIndexStyle}
closeOnSwipe
>
<div className={styles.header}>
{/* used to make the title center */}
<span className={styles.placeholder}>
<CloseOutline />
</span>
<span>{t('Filter')}</span>
<span className={styles.closeIcon} onClick={closePopup}>
<CloseOutline />
</span>
</div>
<div style={{ padding: 12 }}>{props.content}</div>
<div style={{ height: 150 }}></div>
</Popup>
</ConfigProvider>
);
}}
/>
);
});
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<HTMLElement>(() => document.querySelector('.mobile-container'));
const [visiblePopup, setVisiblePopup] = useState(false);
const popupContainerRef = React.useRef<HTMLDivElement>(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,
};
};

View File

@ -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};
`,
};
});

View File

@ -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 (
<BasicZIndexProvider basicZIndex={newZIndex}>
<ConfigProvider theme={theme}>
<div onClick={openPopup}>{props.children}</div>
<Popup
visible={visiblePopup}
onClose={closePopup}
onMaskClick={closePopup}
getContainer={() => popupContainerRef.current as HTMLElement}
bodyClassName={styles.body}
bodyStyle={zIndexStyle}
maskStyle={zIndexStyle}
showCloseButton
closeOnSwipe
>
<div className={styles.header}>
{/* used to make the title center */}
<span className={styles.placeholder}>
<CloseOutline />
</span>
<span>{title}</span>
<span className={styles.closeIcon} onClick={closePopup}>
<CloseOutline />
</span>
</div>
{props.content}
<div style={{ height: 50 }}></div>
</Popup>
</ConfigProvider>
</BasicZIndexProvider>
);
};
export const InternalPopoverNesterUsedInMobile: React.FC<{ OriginComponent: React.FC<any> }> = (props) => {
const { OriginComponent } = props;
return <OriginComponent {...props} Container={Container} />;
};

View File

@ -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 <SchemaComponentOptions scope={scopes}>{props.children}</SchemaComponentOptions>;
};

View File

@ -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;
`,
};
});

View File

@ -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<SchemaInitializer>(() =>
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<SchemaInitializer>(() => {
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 = (
<BasicZIndexProvider basicZIndex={newZIndex}>
<div className={styles.container} style={zIndexStyle}>
<TabsContextProvider {...tabContext} tabBarExtraContent={<BackButtonUsedInSubPage />} tabBarGutter={48}>
<SchemaComponent components={components} schema={fieldSchema} onlyRenderProperties />
</TabsContextProvider>
{footerSchema && (
<div className={styles.footer} style={zIndexStyle}>
<RecursionField
basePath={field.address}
schema={fieldSchema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === footerNodeName;
}}
/>
</div>
)}
</div>
</BasicZIndexProvider>
);
if (containerDOM) {
return createPortal(actionPageNode, containerDOM);
}
return actionPageNode;
};

View File

@ -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] = <SchemaComponent name={key} schema={schema} onlyRenderProperties distributed />;
@ -50,6 +63,7 @@ export const MobileTabsForMobileActionPage: any = observer(
const tabContent = useMemo(() => {
const list = fieldSchema.mapProperties((schema, key) => {
schema = hideDivider(schema);
return {
key,
node: <SchemaComponent name={key} schema={schema} onlyRenderProperties distributed />,
@ -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;
}

View File

@ -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,
},
};
};

View File

@ -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: <Outlet />,
});

View File

@ -22,6 +22,7 @@ export const MobileProviders: FC<MobileProvidersProps> = ({ 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 (

View File

@ -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<MobileRoutesContextValue>(null);
export interface MobileRoutesContextValue {
routeList?: MobileRouteItem[];
@ -37,8 +38,6 @@ export interface MobileRoutesContextValue {
activeTabItem?: MobileRouteItem;
api: APIClient;
}
export const MobileRoutesContext = createContext<MobileRoutesContextValue>(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);

View File

@ -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) => (
<InternalPopoverNesterUsedInMobile {...props} OriginComponent={OriginComponent} />
)),
};
}, []);
return (
<DesktopComponent>
{/* 目前移动端由于和客户端的主题对不上,所以先使用 `GlobalThemeProvider` 和 `AntdAppProvider` 进行重置为默认主题 */}
<div className={styles.nbMobile}>
<GlobalThemeProvider
theme={{
token: {
marginBlock: 18,
borderRadiusBlock: 0,
boxShadowTertiary: 'none',
},
}}
>
<AntdAppProvider>
<OpenModeProvider
defaultOpenMode="page"
hideOpenMode
openModeToComponent={{
page: MobileActionPage,
drawer: MobileActionPage,
modal: Action.Modal,
}}
>
<GlobalThemeProvider
theme={{
token: {
marginBlock: 18,
borderRadiusBlock: 0,
boxShadowTertiary: 'none',
},
}}
>
<AntdAppProvider className={`mobile-container ${styles.nbMobile}`}>
<OpenModeProvider
defaultOpenMode="page"
hideOpenMode
openModeToComponent={{
page: MobileActionPage,
drawer: ActionDrawerUsedInMobile,
modal: Action.Modal,
}}
>
<BlockTemplateProvider componentNamePrefix="mobile-">
<MobileAppProvider>
<MobileRouter />
<ResetSchemaOptionsProvider>
<AssociationFieldModeProvider modeToComponent={modeToComponent}>
{/* the z-index of all popups and subpages will be based on this value */}
<BasicZIndexProvider basicZIndex={1000}>
<MobileRouter />
</BasicZIndexProvider>
</AssociationFieldModeProvider>
</ResetSchemaOptionsProvider>
</MobileAppProvider>
</OpenModeProvider>
</AntdAppProvider>
</GlobalThemeProvider>
</div>
</BlockTemplateProvider>
</OpenModeProvider>
</AntdAppProvider>
</GlobalThemeProvider>
</DesktopComponent>
);
};

View File

@ -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;
}

View File

@ -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',

View File

@ -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 = (
<div className={styles.container} style={style}>
<TabsContextProvider {...tabContext} tabBarExtraContent={<BackButtonUsedInSubPage />} tabBarGutter={48}>
<SchemaComponent components={components} schema={filedSchema} onlyRenderProperties />
</TabsContextProvider>
</div>
);
if (containerDOM) {
return createPortal(actionPageNode, containerDOM);
}
return actionPageNode;
};

Some files were not shown because too many files have changed in this diff Show More