mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 21:49:25 +08:00
feat: adapt styles for mobile devices (#6393)
* feat(layout): integrate ConfigProvider for theme management in admin layout * feat(layout): add mobile layout context and transform utility for schema * feat(toolbar): disable dragging in mobile layout for BlockSchemaToolbar * test: add unit tests for transformMultiColumnToSingleColumn function * feat: add mobile layout support to form schema transformation * feat: support mobile layout in Tabs component schema transformation * feat: integrate mobile layout support into designable components * feat: refactor mobile layout hooks and integrate MobileComponentsProvider * feat: update mobile layout styles and integrate global theme in ActionDrawer * feat: update Mobile component to use CommonDrawer for popup handling in mobile layout * feat: adjust padding in ActionDrawer style for improved layout on mobile * feat: optimize scope usage in InternalPicker for improved performance * feat: update Action.Page style to improve overflow handling * style: remove minHeight from ActionDrawer style * feat: add mobile layout support and transform schema for multi-column fields * feat: support mobile components * fix(mobile): issue with mobile flash/flicker * feat: hide Plugin manager and Settings center * feat: hide scroll bar in mobile * feat(todos): support to collapse sider menu * feat: enhance mobile layout theme support with dark mode
This commit is contained in:
parent
37f8936781
commit
c2786c1bee
@ -14,6 +14,7 @@ import { useCollection } from '../../data-source/collection/CollectionProvider';
|
|||||||
import { useCompile } from '../../schema-component';
|
import { useCompile } from '../../schema-component';
|
||||||
import { SchemaToolbar } from '../../schema-settings/GeneralSchemaDesigner';
|
import { SchemaToolbar } from '../../schema-settings/GeneralSchemaDesigner';
|
||||||
import { useSchemaTemplate } from '../../schema-templates';
|
import { useSchemaTemplate } from '../../schema-templates';
|
||||||
|
import { useMobileLayout } from '../../route-switch/antd/admin-layout';
|
||||||
|
|
||||||
export const BlockSchemaToolbar = (props) => {
|
export const BlockSchemaToolbar = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -22,6 +23,7 @@ export const BlockSchemaToolbar = (props) => {
|
|||||||
const template = useSchemaTemplate();
|
const template = useSchemaTemplate();
|
||||||
const { association, collection } = useDataBlockProps() || {};
|
const { association, collection } = useDataBlockProps() || {};
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
if (association) {
|
if (association) {
|
||||||
const [collectionName] = association.split('.');
|
const [collectionName] = association.split('.');
|
||||||
@ -51,7 +53,7 @@ export const BlockSchemaToolbar = (props) => {
|
|||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
}, [currentCollectionTitle, currentCollectionName, associationField, associationCollection, compile, templateName]);
|
}, [currentCollectionTitle, currentCollectionName, associationField, associationCollection, compile, templateName]);
|
||||||
|
|
||||||
return <SchemaToolbar title={toolbarTitle} {...props} />;
|
return <SchemaToolbar title={toolbarTitle} {...props} draggable={!isMobileLayout} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getCollectionTitle(arg: {
|
export function getCollectionTitle(arg: {
|
||||||
|
@ -13,6 +13,7 @@ import React, { useEffect, useMemo } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useApp, useNavigateNoUpdate } from '../application';
|
import { useApp, useNavigateNoUpdate } from '../application';
|
||||||
|
import { useMobileLayout } from '../route-switch/antd/admin-layout';
|
||||||
import { useCompile } from '../schema-component';
|
import { useCompile } from '../schema-component';
|
||||||
import { useToken } from '../style';
|
import { useToken } from '../style';
|
||||||
|
|
||||||
@ -20,6 +21,12 @@ export const PluginManagerLink = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigateNoUpdate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const { token } = useToken();
|
const { token } = useToken();
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
|
if (isMobileLayout) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t('Plugin manager')}>
|
<Tooltip title={t('Plugin manager')}>
|
||||||
<Button
|
<Button
|
||||||
@ -62,6 +69,12 @@ export const SettingsCenterDropdown = () => {
|
|||||||
};
|
};
|
||||||
}, [app.pluginSettingsManager]);
|
}, [app.pluginSettingsManager]);
|
||||||
|
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
|
if (isMobileLayout) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{
|
menu={{
|
||||||
@ -75,7 +88,7 @@ export const SettingsCenterDropdown = () => {
|
|||||||
<Button
|
<Button
|
||||||
data-testid="plugin-settings-button"
|
data-testid="plugin-settings-button"
|
||||||
icon={<SettingOutlined style={{ color: token.colorTextHeaderMenu }} />}
|
icon={<SettingOutlined style={{ color: token.colorTextHeaderMenu }} />}
|
||||||
// title={t('All plugin settings')}
|
// title={t('All plugin settings')}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
@ -11,7 +11,7 @@ import { EllipsisOutlined } from '@ant-design/icons';
|
|||||||
import ProLayout, { RouteContext, RouteContextType } from '@ant-design/pro-layout';
|
import ProLayout, { RouteContext, RouteContextType } from '@ant-design/pro-layout';
|
||||||
import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header';
|
import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { Popover, Tooltip } from 'antd';
|
import { theme as antdTheme, ConfigProvider, Popover, Tooltip } from 'antd';
|
||||||
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { Link, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
import { Link, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
@ -29,6 +29,7 @@ import {
|
|||||||
RemoteSchemaTemplateManagerProvider,
|
RemoteSchemaTemplateManagerProvider,
|
||||||
SortableItem,
|
SortableItem,
|
||||||
useDesignable,
|
useDesignable,
|
||||||
|
useGlobalTheme,
|
||||||
useMenuDragEnd,
|
useMenuDragEnd,
|
||||||
useParseURLAndParams,
|
useParseURLAndParams,
|
||||||
useRequest,
|
useRequest,
|
||||||
@ -73,7 +74,7 @@ const AllAccessDesktopRoutesContext = createContext<{
|
|||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
}>({
|
}>({
|
||||||
allAccessRoutes: emptyArray,
|
allAccessRoutes: emptyArray,
|
||||||
refresh: () => {},
|
refresh: () => { },
|
||||||
});
|
});
|
||||||
AllAccessDesktopRoutesContext.displayName = 'AllAccessDesktopRoutesContext';
|
AllAccessDesktopRoutesContext.displayName = 'AllAccessDesktopRoutesContext';
|
||||||
|
|
||||||
@ -495,22 +496,44 @@ const headerRender = (props: HeaderViewProps, defaultDom: React.ReactNode) => {
|
|||||||
return <headerContext.Provider value={headerContextValue}>{defaultDom}</headerContext.Provider>;
|
return <headerContext.Provider value={headerContextValue}>{defaultDom}</headerContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const IsMobileLayoutContext = React.createContext<{
|
||||||
|
isMobileLayout: boolean;
|
||||||
|
setIsMobileLayout: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}>({
|
||||||
|
isMobileLayout: false,
|
||||||
|
setIsMobileLayout: () => { },
|
||||||
|
});
|
||||||
|
|
||||||
|
const MobileLayoutProvider: FC = (props) => {
|
||||||
|
const [isMobileLayout, setIsMobileLayout] = useState(false);
|
||||||
|
const value = useMemo(() => ({ isMobileLayout, setIsMobileLayout }), [isMobileLayout]);
|
||||||
|
|
||||||
|
return <IsMobileLayoutContext.Provider value={value}>{props.children}</IsMobileLayoutContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMobileLayout = () => {
|
||||||
|
const { isMobileLayout, setIsMobileLayout } = useContext(IsMobileLayoutContext);
|
||||||
|
return { isMobileLayout, setIsMobileLayout };
|
||||||
|
};
|
||||||
|
|
||||||
export const InternalAdminLayout = () => {
|
export const InternalAdminLayout = () => {
|
||||||
const { allAccessRoutes } = useAllAccessDesktopRoutes();
|
const { allAccessRoutes } = useAllAccessDesktopRoutes();
|
||||||
const { designable } = useDesignable();
|
const { designable: _designable } = useDesignable();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { onDragEnd } = useMenuDragEnd();
|
const { onDragEnd } = useMenuDragEnd();
|
||||||
const { token } = useToken();
|
const { token } = useToken();
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const { isMobileLayout, setIsMobileLayout } = useMobileLayout();
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(isMobileLayout);
|
||||||
const doNotChangeCollapsedRef = useRef(false);
|
const doNotChangeCollapsedRef = useRef(false);
|
||||||
const { t } = useMenuTranslation();
|
const { t } = useMenuTranslation();
|
||||||
|
const designable = isMobileLayout ? false : _designable;
|
||||||
|
|
||||||
const route = useMemo(() => {
|
const route = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
path: '/',
|
path: '/',
|
||||||
children: convertRoutesToLayout(allAccessRoutes, { designable, isMobile, t }),
|
children: convertRoutesToLayout(allAccessRoutes, { designable, isMobile: isMobileLayout, t }),
|
||||||
};
|
};
|
||||||
}, [allAccessRoutes, designable, isMobile, t]);
|
}, [allAccessRoutes, designable, isMobileLayout, t]);
|
||||||
const layoutToken = useMemo(() => {
|
const layoutToken = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
header: {
|
header: {
|
||||||
@ -534,6 +557,21 @@ export const InternalAdminLayout = () => {
|
|||||||
bgLayout: token.colorBgLayout,
|
bgLayout: token.colorBgLayout,
|
||||||
};
|
};
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
const { theme, isDarkTheme } = useGlobalTheme();
|
||||||
|
const mobileTheme = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...theme,
|
||||||
|
token: {
|
||||||
|
...theme.token,
|
||||||
|
paddingPageHorizontal: 8, // Horizontal page padding
|
||||||
|
paddingPageVertical: 8, // Vertical page padding
|
||||||
|
marginBlock: 12, // Spacing between blocks
|
||||||
|
borderRadiusBlock: 8, // Block border radius
|
||||||
|
fontSize: 14, // Font size
|
||||||
|
},
|
||||||
|
algorithm: isDarkTheme ? [antdTheme.compactAlgorithm, antdTheme.darkAlgorithm] : antdTheme.compactAlgorithm, // Set mobile mode to always use compact algorithm
|
||||||
|
};
|
||||||
|
}, [theme, isDarkTheme]);
|
||||||
|
|
||||||
const onCollapse = useCallback((collapsed: boolean) => {
|
const onCollapse = useCallback((collapsed: boolean) => {
|
||||||
if (doNotChangeCollapsedRef.current) {
|
if (doNotChangeCollapsedRef.current) {
|
||||||
@ -575,11 +613,15 @@ export const InternalAdminLayout = () => {
|
|||||||
{(value: RouteContextType) => {
|
{(value: RouteContextType) => {
|
||||||
const { isMobile: _isMobile } = value;
|
const { isMobile: _isMobile } = value;
|
||||||
|
|
||||||
if (_isMobile !== isMobile) {
|
if (_isMobile !== isMobileLayout) {
|
||||||
setIsMobile(_isMobile);
|
setIsMobileLayout(_isMobile);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <LayoutContent />;
|
return (
|
||||||
|
<ConfigProvider theme={_isMobile ? mobileTheme : theme}>
|
||||||
|
<LayoutContent />
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
</RouteContext.Consumer>
|
</RouteContext.Consumer>
|
||||||
</ProLayout>
|
</ProLayout>
|
||||||
@ -696,6 +738,7 @@ export class AdminLayoutPlugin extends Plugin {
|
|||||||
async load() {
|
async load() {
|
||||||
this.app.schemaSettingsManager.add(userCenterSettings);
|
this.app.schemaSettingsManager.add(userCenterSettings);
|
||||||
this.app.addComponents({ AdminLayout, AdminDynamicPage });
|
this.app.addComponents({ AdminLayout, AdminDynamicPage });
|
||||||
|
this.app.use(MobileLayoutProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -717,36 +760,6 @@ export function findRouteBySchemaUid(schemaUid: string, treeArray: any[]) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuItemIcon: FC<{ icon: string; title: string }> = (props) => {
|
|
||||||
const { inHeader } = useContext(headerContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RouteContext.Consumer>
|
|
||||||
{(value: RouteContextType) => {
|
|
||||||
const { collapsed } = value;
|
|
||||||
|
|
||||||
if (collapsed && !inHeader) {
|
|
||||||
return props.icon ? (
|
|
||||||
<Icon type={props.icon} />
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: 'inline-block',
|
|
||||||
width: '100%',
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.title.charAt(0)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.icon ? <Icon type={props.icon} /> : null;
|
|
||||||
}}
|
|
||||||
</RouteContext.Consumer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MenuDesignerButton: FC<{ testId: string }> = (props) => {
|
const MenuDesignerButton: FC<{ testId: string }> = (props) => {
|
||||||
const { render: renderInitializer } = useSchemaInitializerRender(menuItemInitializer);
|
const { render: renderInitializer } = useSchemaInitializerRender(menuItemInitializer);
|
||||||
|
|
||||||
@ -766,7 +779,7 @@ const MenuTitleWithIcon: FC<{ icon: any; title: string }> = (props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.title;
|
return <>{props.title}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function convertRoutesToLayout(
|
function convertRoutesToLayout(
|
||||||
|
@ -20,7 +20,8 @@ export const useActionPageStyle = genStyleHook('nb-action-page', (token) => {
|
|||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
backgroundColor: token.colorBgLayout,
|
backgroundColor: token.colorBgLayout,
|
||||||
overflow: 'auto',
|
overflowX: 'hidden',
|
||||||
|
overflowY: 'auto',
|
||||||
|
|
||||||
'.ant-tabs-nav': {
|
'.ant-tabs-nav': {
|
||||||
background: token.colorBgContainer,
|
background: token.colorBgContainer,
|
||||||
|
@ -13,6 +13,7 @@ import { differenceBy, unionBy } from 'lodash';
|
|||||||
import React, { useContext, useMemo, useState } from 'react';
|
import React, { useContext, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
FormProvider,
|
FormProvider,
|
||||||
|
PopupSettingsProvider,
|
||||||
RecordPickerContext,
|
RecordPickerContext,
|
||||||
RecordPickerProvider,
|
RecordPickerProvider,
|
||||||
SchemaComponentOptions,
|
SchemaComponentOptions,
|
||||||
@ -24,6 +25,7 @@ import {
|
|||||||
NocoBaseRecursionField,
|
NocoBaseRecursionField,
|
||||||
RecordProvider,
|
RecordProvider,
|
||||||
useCollectionRecordData,
|
useCollectionRecordData,
|
||||||
|
useMobileLayout,
|
||||||
} from '../../..';
|
} from '../../..';
|
||||||
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
|
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
|
||||||
import {
|
import {
|
||||||
@ -35,6 +37,7 @@ import { ActionContextProvider } from '../action';
|
|||||||
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
||||||
import schema from './schema';
|
import schema from './schema';
|
||||||
import { flatData, getLabelFormatValue, useLabelUiSchema } from './util';
|
import { flatData, getLabelFormatValue, useLabelUiSchema } from './util';
|
||||||
|
import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
|
||||||
|
|
||||||
export const useTableSelectorProps = () => {
|
export const useTableSelectorProps = () => {
|
||||||
const field: any = useField();
|
const field: any = useField();
|
||||||
@ -117,6 +120,7 @@ export const InternalPicker = observer(
|
|||||||
collectionField,
|
collectionField,
|
||||||
currentFormCollection: collectionName,
|
currentFormCollection: collectionName,
|
||||||
};
|
};
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
const getValue = () => {
|
const getValue = () => {
|
||||||
if (multiple == null) return null;
|
if (multiple == null) return null;
|
||||||
@ -147,8 +151,16 @@ export const InternalPicker = observer(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
const scope = useMemo(
|
||||||
|
() => ({
|
||||||
|
usePickActionProps,
|
||||||
|
useTableSelectorProps,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PopupSettingsProvider enableURL={false}>
|
||||||
<Space.Compact style={{ display: 'flex', lineHeight: '32px' }}>
|
<Space.Compact style={{ display: 'flex', lineHeight: '32px' }}>
|
||||||
<div style={{ width: '100%' }}>
|
<div style={{ width: '100%' }}>
|
||||||
<Select
|
<Select
|
||||||
@ -213,16 +225,11 @@ export const InternalPicker = observer(
|
|||||||
<CollectionProvider_deprecated name={collectionField?.target}>
|
<CollectionProvider_deprecated name={collectionField?.target}>
|
||||||
<FormProvider>
|
<FormProvider>
|
||||||
<TableSelectorParamsProvider params={{ filter: getFilter() }}>
|
<TableSelectorParamsProvider params={{ filter: getFilter() }}>
|
||||||
<SchemaComponentOptions
|
<SchemaComponentOptions scope={scope}>
|
||||||
scope={{
|
|
||||||
usePickActionProps,
|
|
||||||
useTableSelectorProps,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NocoBaseRecursionField
|
<NocoBaseRecursionField
|
||||||
onlyRenderProperties
|
onlyRenderProperties
|
||||||
basePath={field.address}
|
basePath={field.address}
|
||||||
schema={fieldSchema}
|
schema={isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema}
|
||||||
filterProperties={(s) => {
|
filterProperties={(s) => {
|
||||||
return s['x-component'] === 'AssociationField.Selector';
|
return s['x-component'] === 'AssociationField.Selector';
|
||||||
}}
|
}}
|
||||||
@ -233,7 +240,7 @@ export const InternalPicker = observer(
|
|||||||
</CollectionProvider_deprecated>
|
</CollectionProvider_deprecated>
|
||||||
</RecordPickerProvider>
|
</RecordPickerProvider>
|
||||||
</ActionContextProvider>
|
</ActionContextProvider>
|
||||||
</>
|
</PopupSettingsProvider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ displayName: 'InternalPicker' },
|
{ displayName: 'InternalPicker' },
|
||||||
|
@ -28,6 +28,8 @@ import { useLocalVariables, useVariables } from '../../../variables';
|
|||||||
import { useProps } from '../../hooks/useProps';
|
import { useProps } from '../../hooks/useProps';
|
||||||
import { useFormBlockHeight } from './hook';
|
import { useFormBlockHeight } from './hook';
|
||||||
import { useApp } from '../../../application';
|
import { useApp } from '../../../application';
|
||||||
|
import { useMobileLayout } from '../../../route-switch/antd/admin-layout';
|
||||||
|
import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
|
||||||
|
|
||||||
export interface FormProps extends IFormLayoutProps {
|
export interface FormProps extends IFormLayoutProps {
|
||||||
form?: FormilyForm;
|
form?: FormilyForm;
|
||||||
@ -49,6 +51,8 @@ const FormComponent: React.FC<FormProps> = (props) => {
|
|||||||
labelWidth = 120,
|
labelWidth = 120,
|
||||||
labelWrap = true,
|
labelWrap = true,
|
||||||
} = cardItemSchema?.['x-component-props'] || {};
|
} = cardItemSchema?.['x-component-props'] || {};
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldContext.Provider value={undefined}>
|
<FieldContext.Provider value={undefined}>
|
||||||
<FormContext.Provider value={form}>
|
<FormContext.Provider value={form}>
|
||||||
@ -76,7 +80,12 @@ const FormComponent: React.FC<FormProps> = (props) => {
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<NocoBaseRecursionField basePath={f.address} schema={fieldSchema} onlyRenderProperties isUseFormilyField />
|
<NocoBaseRecursionField
|
||||||
|
basePath={f.address}
|
||||||
|
schema={isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema}
|
||||||
|
onlyRenderProperties
|
||||||
|
isUseFormilyField
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FormLayout>
|
</FormLayout>
|
||||||
</FormContext.Provider>
|
</FormContext.Provider>
|
||||||
@ -93,6 +102,8 @@ const FormDecorator: React.FC<FormProps> = (props) => {
|
|||||||
// TODO: component 里 useField 会与当前 field 存在偏差
|
// TODO: component 里 useField 会与当前 field 存在偏差
|
||||||
const f = useAttach(form.createVoidField({ ...field.props, basePath: '' }));
|
const f = useAttach(form.createVoidField({ ...field.props, basePath: '' }));
|
||||||
const Component = useComponent(fieldSchema['x-component'], Def);
|
const Component = useComponent(fieldSchema['x-component'], Def);
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldContext.Provider value={undefined}>
|
<FieldContext.Provider value={undefined}>
|
||||||
<FormContext.Provider value={form}>
|
<FormContext.Provider value={form}>
|
||||||
@ -101,7 +112,7 @@ const FormDecorator: React.FC<FormProps> = (props) => {
|
|||||||
<Component {...field.componentProps}>
|
<Component {...field.componentProps}>
|
||||||
<NocoBaseRecursionField
|
<NocoBaseRecursionField
|
||||||
basePath={f.address}
|
basePath={f.address}
|
||||||
schema={fieldSchema}
|
schema={isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema}
|
||||||
onlyRenderProperties
|
onlyRenderProperties
|
||||||
isUseFormilyField
|
isUseFormilyField
|
||||||
/>
|
/>
|
||||||
@ -250,7 +261,7 @@ export const Form: React.FC<FormProps> & {
|
|||||||
const theme: any = useMemo(() => {
|
const theme: any = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
token: {
|
token: {
|
||||||
// 这里是为了防止区块内部也收到 marginBlock 的影响(marginBlock:区块之间的间距)
|
// 这里是为了防止区块内部也受到 marginBlock 的影响(marginBlock:区块之间的间距)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
marginBlock: token.marginLG,
|
marginBlock: token.marginLG,
|
||||||
},
|
},
|
||||||
|
@ -25,6 +25,8 @@ import { ListDesigner } from './List.Designer';
|
|||||||
import { ListItem } from './List.Item';
|
import { ListItem } from './List.Item';
|
||||||
import useStyles from './List.style';
|
import useStyles from './List.style';
|
||||||
import { useListActionBarProps, useListBlockHeight } from './hooks';
|
import { useListActionBarProps, useListBlockHeight } from './hooks';
|
||||||
|
import { useMobileLayout } from '../../../route-switch/antd/admin-layout';
|
||||||
|
import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
|
||||||
|
|
||||||
const InternalList = withSkeletonComponent(
|
const InternalList = withSkeletonComponent(
|
||||||
(props) => {
|
(props) => {
|
||||||
@ -121,6 +123,8 @@ const InternalList = withSkeletonComponent(
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
const paginationProps = usePagination();
|
const paginationProps = usePagination();
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
return wrapSSR(
|
return wrapSSR(
|
||||||
<SchemaComponentOptions
|
<SchemaComponentOptions
|
||||||
scope={{
|
scope={{
|
||||||
@ -165,7 +169,9 @@ const InternalList = withSkeletonComponent(
|
|||||||
key={index}
|
key={index}
|
||||||
name={index}
|
name={index}
|
||||||
onlyRenderProperties
|
onlyRenderProperties
|
||||||
schema={getSchema(index)}
|
schema={
|
||||||
|
isMobileLayout ? transformMultiColumnToSingleColumn(getSchema(index)) : getSchema(index)
|
||||||
|
}
|
||||||
></NocoBaseRecursionField>
|
></NocoBaseRecursionField>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -37,6 +37,7 @@ import {
|
|||||||
NocoBaseDesktopRouteType,
|
NocoBaseDesktopRouteType,
|
||||||
NocoBaseRouteContext,
|
NocoBaseRouteContext,
|
||||||
useCurrentRoute,
|
useCurrentRoute,
|
||||||
|
useMobileLayout,
|
||||||
} from '../../../route-switch/antd/admin-layout';
|
} from '../../../route-switch/antd/admin-layout';
|
||||||
import { KeepAliveProvider, useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
|
import { KeepAliveProvider, useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
|
||||||
import { useGetAriaLabelOfSchemaInitializer } from '../../../schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer';
|
import { useGetAriaLabelOfSchemaInitializer } from '../../../schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer';
|
||||||
@ -50,6 +51,7 @@ import { useMenuDragEnd, useNocoBaseRoutes } from '../menu/Menu';
|
|||||||
import { useStyles } from './Page.style';
|
import { useStyles } from './Page.style';
|
||||||
import { PageDesigner, PageTabDesigner } from './PageTabDesigner';
|
import { PageDesigner, PageTabDesigner } from './PageTabDesigner';
|
||||||
import { PopupRouteContextResetter } from './PopupRouteContextResetter';
|
import { PopupRouteContextResetter } from './PopupRouteContextResetter';
|
||||||
|
import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
currentTabUid: string;
|
currentTabUid: string;
|
||||||
@ -174,6 +176,7 @@ const displayNone = {
|
|||||||
const TabPane = React.memo(({ active: tabActive, uid }: { active: boolean; uid: string }) => {
|
const TabPane = React.memo(({ active: tabActive, uid }: { active: boolean; uid: string }) => {
|
||||||
const mountedRef = useRef(false);
|
const mountedRef = useRef(false);
|
||||||
const { active: pageActive } = useKeepAlive();
|
const { active: pageActive } = useKeepAlive();
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
if (tabActive && !mountedRef.current) {
|
if (tabActive && !mountedRef.current) {
|
||||||
mountedRef.current = true;
|
mountedRef.current = true;
|
||||||
@ -186,7 +189,10 @@ const TabPane = React.memo(({ active: tabActive, uid }: { active: boolean; uid:
|
|||||||
return (
|
return (
|
||||||
<div style={tabActive ? displayBlock : displayNone}>
|
<div style={tabActive ? displayBlock : displayNone}>
|
||||||
<KeepAliveProvider active={pageActive && tabActive}>
|
<KeepAliveProvider active={pageActive && tabActive}>
|
||||||
<RemoteSchemaComponent uid={uid} />
|
<RemoteSchemaComponent
|
||||||
|
uid={uid}
|
||||||
|
schemaTransform={isMobileLayout ? transformMultiColumnToSingleColumn : undefined}
|
||||||
|
/>
|
||||||
</KeepAliveProvider>
|
</KeepAliveProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -204,6 +210,7 @@ const InternalPageContent = (props: PageContentProps) => {
|
|||||||
const currentRoute = useCurrentRoute();
|
const currentRoute = useCurrentRoute();
|
||||||
const navigate = useNavigateNoUpdate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const location = useLocationNoUpdate();
|
const location = useLocationNoUpdate();
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
const children = currentRoute?.children || [];
|
const children = currentRoute?.children || [];
|
||||||
const noTabs = children.every((tabRoute) => tabRoute.schemaUid !== activeKey && tabRoute.tabSchemaName !== activeKey);
|
const noTabs = children.every((tabRoute) => tabRoute.schemaUid !== activeKey && tabRoute.tabSchemaName !== activeKey);
|
||||||
@ -244,7 +251,10 @@ const InternalPageContent = (props: PageContentProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className={className1}>
|
<div className={className1}>
|
||||||
<NocoBaseRouteContext.Provider value={currentRoute?.children?.[0]}>
|
<NocoBaseRouteContext.Provider value={currentRoute?.children?.[0]}>
|
||||||
<RemoteSchemaComponent uid={currentRoute?.children?.[0].schemaUid} />
|
<RemoteSchemaComponent
|
||||||
|
uid={currentRoute?.children?.[0].schemaUid}
|
||||||
|
schemaTransform={isMobileLayout ? transformMultiColumnToSingleColumn : undefined}
|
||||||
|
/>
|
||||||
</NocoBaseRouteContext.Provider>
|
</NocoBaseRouteContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -17,6 +17,7 @@ const PopupSettingsContext = React.createContext({
|
|||||||
export const PopupSettingsProvider: FC<{
|
export const PopupSettingsProvider: FC<{
|
||||||
/**
|
/**
|
||||||
* @default true
|
* @default true
|
||||||
|
* Whether the popup should be controlled by URL
|
||||||
*/
|
*/
|
||||||
enableURL?: boolean;
|
enableURL?: boolean;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
@ -52,7 +53,7 @@ export const usePopupSettings = () => {
|
|||||||
}, [enableURL, isInSettingsPage]);
|
}, [enableURL, isInSettingsPage]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/** 弹窗窗口的显隐是否由 URL 控制 */
|
/** Whether the visibility of the popup window is controlled by URL */
|
||||||
isPopupVisibleControlledByURL,
|
isPopupVisibleControlledByURL,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||||
import { Tabs as AntdTabs, TabPaneProps, TabsProps } from 'antd';
|
import { Tabs as AntdTabs, TabPaneProps, TabsProps } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
@ -20,6 +20,8 @@ import { SchemaComponent } from '../../core';
|
|||||||
import { useDesigner } from '../../hooks/useDesigner';
|
import { useDesigner } from '../../hooks/useDesigner';
|
||||||
import { useTabsContext } from './context';
|
import { useTabsContext } from './context';
|
||||||
import { TabsDesigner } from './Tabs.Designer';
|
import { TabsDesigner } from './Tabs.Designer';
|
||||||
|
import { useMobileLayout } from '../../../route-switch/antd/admin-layout';
|
||||||
|
import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
|
||||||
|
|
||||||
const MemoizeRecursionField = React.memo(RecursionField);
|
const MemoizeRecursionField = React.memo(RecursionField);
|
||||||
MemoizeRecursionField.displayName = 'MemoizeRecursionField';
|
MemoizeRecursionField.displayName = 'MemoizeRecursionField';
|
||||||
@ -32,6 +34,7 @@ export const Tabs: any = React.memo((props: TabsProps) => {
|
|||||||
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']);
|
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']);
|
||||||
const contextProps = useTabsContext();
|
const contextProps = useTabsContext();
|
||||||
const { PaneRoot = React.Fragment as React.FC<any> } = contextProps;
|
const { PaneRoot = React.Fragment as React.FC<any> } = contextProps;
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const result = fieldSchema.mapProperties((schema, key: string) => {
|
const result = fieldSchema.mapProperties((schema, key: string) => {
|
||||||
@ -40,14 +43,19 @@ export const Tabs: any = React.memo((props: TabsProps) => {
|
|||||||
label: <MemoizeRecursionField name={key} schema={schema} onlyRenderSelf />,
|
label: <MemoizeRecursionField name={key} schema={schema} onlyRenderSelf />,
|
||||||
children: (
|
children: (
|
||||||
<PaneRoot key={key} {...(PaneRoot !== React.Fragment ? { active: key === contextProps.activeKey } : {})}>
|
<PaneRoot key={key} {...(PaneRoot !== React.Fragment ? { active: key === contextProps.activeKey } : {})}>
|
||||||
<SchemaComponent name={key} schema={schema} onlyRenderProperties distributed />
|
<SchemaComponent
|
||||||
|
name={key}
|
||||||
|
schema={isMobileLayout ? new Schema(transformMultiColumnToSingleColumn(schema)) : schema}
|
||||||
|
onlyRenderProperties
|
||||||
|
distributed
|
||||||
|
/>
|
||||||
</PaneRoot>
|
</PaneRoot>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [fieldSchema]);
|
}, [fieldSchema, isMobileLayout]);
|
||||||
|
|
||||||
const tabBarExtraContent = useMemo(
|
const tabBarExtraContent = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
@ -14,6 +14,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDesignable } from '..';
|
import { useDesignable } from '..';
|
||||||
import { useToken } from '../../style';
|
import { useToken } from '../../style';
|
||||||
|
import { useMobileLayout } from '../../route-switch/antd/admin-layout';
|
||||||
|
|
||||||
const designableStyle = {
|
const designableStyle = {
|
||||||
backgroundColor: 'var(--colorSettings) !important',
|
backgroundColor: 'var(--colorSettings) !important',
|
||||||
@ -28,10 +29,15 @@ export const DesignableSwitch = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { token } = useToken();
|
const { token } = useToken();
|
||||||
const style = designable ? designableStyle : unDesignableStyle;
|
const style = designable ? designableStyle : unDesignableStyle;
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
// 快捷键切换编辑状态
|
// 快捷键切换编辑状态
|
||||||
useHotkeys('Ctrl+Shift+U', () => setDesignable(!designable), [designable]);
|
useHotkeys('Ctrl+Shift+U', () => setDesignable(!designable), [designable]);
|
||||||
|
|
||||||
|
if (isMobileLayout) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t('UI Editor')}>
|
<Tooltip title={t('UI Editor')}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -25,6 +25,7 @@ import { addAppVersion } from './addAppVersion';
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import clientPkg from '../../../package.json';
|
import clientPkg from '../../../package.json';
|
||||||
|
import { useMobileLayout } from '../../route-switch/antd/admin-layout';
|
||||||
|
|
||||||
interface CreateDesignableProps {
|
interface CreateDesignableProps {
|
||||||
current: Schema;
|
current: Schema;
|
||||||
@ -780,9 +781,11 @@ export function useDesignable() {
|
|||||||
dn.loadAPIClientEvents();
|
dn.loadAPIClientEvents();
|
||||||
}, [dn]);
|
}, [dn]);
|
||||||
|
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dn,
|
dn,
|
||||||
designable,
|
designable: isMobileLayout ? false : designable,
|
||||||
reset,
|
reset,
|
||||||
refresh,
|
refresh,
|
||||||
setDesignable,
|
setDesignable,
|
||||||
|
@ -9,9 +9,16 @@
|
|||||||
|
|
||||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { BlockTemplateProvider, CollectionDeletedPlaceholder, RemoteSchemaComponent, useDesignable } from '..';
|
import {
|
||||||
|
BlockTemplateProvider,
|
||||||
|
CollectionDeletedPlaceholder,
|
||||||
|
RemoteSchemaComponent,
|
||||||
|
useDesignable,
|
||||||
|
useMobileLayout,
|
||||||
|
} from '..';
|
||||||
import { useTemplateBlockContext } from '../block-provider/TemplateBlockProvider';
|
import { useTemplateBlockContext } from '../block-provider/TemplateBlockProvider';
|
||||||
import { useSchemaTemplateManager } from './SchemaTemplateManagerProvider';
|
import { useSchemaTemplateManager } from './SchemaTemplateManagerProvider';
|
||||||
|
import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
|
||||||
|
|
||||||
export const BlockTemplate = observer(
|
export const BlockTemplate = observer(
|
||||||
(props: any) => {
|
(props: any) => {
|
||||||
@ -22,6 +29,7 @@ export const BlockTemplate = observer(
|
|||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
const template = useMemo(() => getTemplateById(templateId), [templateId]);
|
const template = useMemo(() => getTemplateById(templateId), [templateId]);
|
||||||
const { onTemplateSuccess } = useTemplateBlockContext();
|
const { onTemplateSuccess } = useTemplateBlockContext();
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
const onSuccess = (data) => {
|
const onSuccess = (data) => {
|
||||||
fieldSchema['x-linkage-rules'] = data?.data?.['x-linkage-rules'] || [];
|
fieldSchema['x-linkage-rules'] = data?.data?.['x-linkage-rules'] || [];
|
||||||
@ -30,7 +38,12 @@ export const BlockTemplate = observer(
|
|||||||
};
|
};
|
||||||
return template ? (
|
return template ? (
|
||||||
<BlockTemplateProvider {...{ dn, field, fieldSchema, template }}>
|
<BlockTemplateProvider {...{ dn, field, fieldSchema, template }}>
|
||||||
<RemoteSchemaComponent noForm uid={template?.uid} onSuccess={onSuccess} />
|
<RemoteSchemaComponent
|
||||||
|
noForm
|
||||||
|
uid={template?.uid}
|
||||||
|
onSuccess={onSuccess}
|
||||||
|
schemaTransform={isMobileLayout ? transformMultiColumnToSingleColumn : undefined}
|
||||||
|
/>
|
||||||
</BlockTemplateProvider>
|
</BlockTemplateProvider>
|
||||||
) : (
|
) : (
|
||||||
<CollectionDeletedPlaceholder type="Block template" name={templateId} />
|
<CollectionDeletedPlaceholder type="Block template" name={templateId} />
|
||||||
|
@ -0,0 +1,207 @@
|
|||||||
|
/**
|
||||||
|
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { transformMultiColumnToSingleColumn } from '../transformMultiColumnToSingleColumn';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('../uid', () => ({
|
||||||
|
uid: vi.fn(() => 'mocked-uid'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock package.json
|
||||||
|
vi.mock('../../package.json', () => ({
|
||||||
|
default: { version: '1.0.0' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('transformMultiColumnToSingleColumn', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return schema as is if null or undefined', () => {
|
||||||
|
expect(transformMultiColumnToSingleColumn(null)).toBeNull();
|
||||||
|
expect(transformMultiColumnToSingleColumn(undefined)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should recursively process non-Grid schemas', () => {
|
||||||
|
const schema = {
|
||||||
|
'x-component': 'Form',
|
||||||
|
properties: {
|
||||||
|
field1: {
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
grid1: {
|
||||||
|
'x-component': 'Grid',
|
||||||
|
properties: {
|
||||||
|
row1: {
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
col1: { 'x-component': 'Input' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = transformMultiColumnToSingleColumn(schema);
|
||||||
|
|
||||||
|
// The non-Grid top level should remain, but child Grid should be transformed
|
||||||
|
expect(result['x-component']).toBe('Form');
|
||||||
|
expect(result.properties.grid1.properties.row1['x-index']).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Grid with non-Grid.Row components', () => {
|
||||||
|
const schema = {
|
||||||
|
'x-component': 'Grid',
|
||||||
|
properties: {
|
||||||
|
nonRow: {
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = transformMultiColumnToSingleColumn(schema);
|
||||||
|
|
||||||
|
expect(result.properties.nonRow['x-index']).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore Grid.Row without properties', () => {
|
||||||
|
const schema = {
|
||||||
|
'x-component': 'Grid',
|
||||||
|
properties: {
|
||||||
|
row1: {
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
// No properties
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = transformMultiColumnToSingleColumn(schema);
|
||||||
|
|
||||||
|
expect(Object.keys(result.properties).length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep Grid.Row with single column as is', () => {
|
||||||
|
const schema = {
|
||||||
|
'x-component': 'Grid',
|
||||||
|
properties: {
|
||||||
|
row1: {
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
col1: { 'x-component': 'Input' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = transformMultiColumnToSingleColumn(schema);
|
||||||
|
|
||||||
|
expect(result.properties.row1['x-index']).toBe(1);
|
||||||
|
expect(Object.keys(result.properties.row1.properties).length).toBe(1);
|
||||||
|
expect(result.properties.row1.properties.col1).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform Grid.Row with multiple columns', () => {
|
||||||
|
const schema = {
|
||||||
|
'x-component': 'Grid',
|
||||||
|
properties: {
|
||||||
|
row1: {
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
col1: { 'x-component': 'Input' },
|
||||||
|
col2: { 'x-component': 'Select' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = transformMultiColumnToSingleColumn(schema);
|
||||||
|
|
||||||
|
// Original row should keep only the first column
|
||||||
|
expect(result.properties.row1['x-index']).toBe(1);
|
||||||
|
expect(Object.keys(result.properties.row1.properties).length).toBe(1);
|
||||||
|
expect(result.properties.row1.properties.col1).toBeDefined();
|
||||||
|
|
||||||
|
// Second column should be transformed into a new row
|
||||||
|
const newRowKey = 'mocked-uid_col2';
|
||||||
|
expect(result.properties[newRowKey]).toBeDefined();
|
||||||
|
expect(result.properties[newRowKey]['x-component']).toBe('Grid.Row');
|
||||||
|
expect(result.properties[newRowKey]['x-index']).toBe(2);
|
||||||
|
expect(result.properties[newRowKey].properties.col2).toBeDefined();
|
||||||
|
expect(result.properties[newRowKey].properties.col2['x-component-props'].width).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex Grid schema with multiple rows and columns', () => {
|
||||||
|
const schema = {
|
||||||
|
'x-component': 'Grid',
|
||||||
|
properties: {
|
||||||
|
row1: {
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
col1: { 'x-component': 'Input' },
|
||||||
|
col2: { 'x-component': 'Select' },
|
||||||
|
col3: { 'x-component': 'Checkbox' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
row2: {
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
col4: { 'x-component': 'DatePicker' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nonRow: {
|
||||||
|
'x-component': 'Divider',
|
||||||
|
},
|
||||||
|
shouldRemove: {
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
// No properties
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = transformMultiColumnToSingleColumn(schema);
|
||||||
|
|
||||||
|
// Check structure
|
||||||
|
expect(Object.keys(result.properties).length).toBe(5); // row1 + 2 new rows from col2,col3 + row2 + nonRow
|
||||||
|
|
||||||
|
// Check row1 (first column stays)
|
||||||
|
expect(result.properties.row1['x-index']).toBe(1);
|
||||||
|
expect(Object.keys(result.properties.row1.properties).length).toBe(1);
|
||||||
|
expect(result.properties.row1.properties.col1).toBeDefined();
|
||||||
|
|
||||||
|
// Check col2 became its own row
|
||||||
|
expect(result.properties['mocked-uid_col2'].properties.col2).toBeDefined();
|
||||||
|
expect(result.properties['mocked-uid_col2'].properties.col2['x-component-props'].width).toBe(100);
|
||||||
|
|
||||||
|
// Check col3 became its own row (second call to uid would return the same mocked value)
|
||||||
|
expect(result.properties['mocked-uid_col3'].properties.col3).toBeDefined();
|
||||||
|
|
||||||
|
// Check row2 stayed intact
|
||||||
|
expect(result.properties.row2['x-index']).toBe(4);
|
||||||
|
expect(Object.keys(result.properties.row2.properties).length).toBe(1);
|
||||||
|
expect(result.properties.row2.properties.col4).toBeDefined();
|
||||||
|
|
||||||
|
// Check nonRow was processed
|
||||||
|
expect(result.properties.nonRow['x-index']).toBe(5);
|
||||||
|
|
||||||
|
// Check shouldRemove was ignored
|
||||||
|
expect(Object.keys(result.properties).includes('shouldRemove')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
@ -30,4 +30,5 @@ export * from './isPortalInBody';
|
|||||||
export * from './parseHTML';
|
export * from './parseHTML';
|
||||||
export * from './uid';
|
export * from './uid';
|
||||||
export * from './url';
|
export * from './url';
|
||||||
|
export * from './transformMultiColumnToSingleColumn';
|
||||||
export { dayjs, lodash };
|
export { dayjs, lodash };
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* 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 { uid } from './uid';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import pkg from '../package.json';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { ISchema, Schema } from '@formily/json-schema';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将多列布局转换为单列布局
|
||||||
|
* @param {Object} schema - 输入的 JSON Schema 对象
|
||||||
|
* @returns {Object} - 转换后的 JSON Schema 对象
|
||||||
|
*/
|
||||||
|
export const transformMultiColumnToSingleColumn = (schema: any): any => {
|
||||||
|
if (!schema) return schema;
|
||||||
|
|
||||||
|
if (schema.toJSON) {
|
||||||
|
schema = schema.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema['x-component'] !== 'Grid') {
|
||||||
|
Object.keys(schema.properties || {}).forEach((key) => {
|
||||||
|
schema.properties[key] = transformMultiColumnToSingleColumn(schema.properties[key]);
|
||||||
|
});
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
schema = _.cloneDeep(schema);
|
||||||
|
|
||||||
|
const newProperties: any = {};
|
||||||
|
const { properties = {} } = schema;
|
||||||
|
let index = 0;
|
||||||
|
Object.keys(properties).forEach((key, rowIndex) => {
|
||||||
|
const row = properties[key];
|
||||||
|
|
||||||
|
if (row['x-component'] !== 'Grid.Row') {
|
||||||
|
row['x-index'] = ++index;
|
||||||
|
newProperties[key] = row;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 忽略没有列的行
|
||||||
|
if (!row.properties) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果一个行只有一列,那么无需展开
|
||||||
|
if (Object.keys(row.properties).length === 1) {
|
||||||
|
row['x-index'] = ++index;
|
||||||
|
newProperties[key] = row;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果一个行有多列,则保留第一列,其余的列需要放到外面形成新的行(每一行依然保持一列)
|
||||||
|
Object.keys(row.properties).forEach((columnKey, colIndex) => {
|
||||||
|
const column = row.properties[columnKey];
|
||||||
|
_.set(column, 'x-component-props.width', 100);
|
||||||
|
|
||||||
|
if (colIndex === 0) {
|
||||||
|
row['x-index'] = ++index;
|
||||||
|
newProperties[key] = row;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete row.properties[columnKey];
|
||||||
|
// 将列转换为行
|
||||||
|
newProperties[`${uid()}_${columnKey}`] = createRow(column, columnKey, ++index);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
schema.properties = newProperties;
|
||||||
|
return schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createRow(column: any, key: string, index: number) {
|
||||||
|
return {
|
||||||
|
type: 'void',
|
||||||
|
version: '2.0',
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
'x-app-version': pkg.version,
|
||||||
|
'x-uid': uid(),
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': index,
|
||||||
|
_isJSONSchemaObject: true,
|
||||||
|
properties: {
|
||||||
|
[key]: column,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -1,9 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { ISchema, useFieldSchema } from '@formily/react';
|
import { ISchema, useFieldSchema } from '@formily/react';
|
||||||
import { Action, ActionContextProvider, SchemaComponent, useCompile } from '@nocobase/client';
|
import { Action, ActionContextProvider, PopupSettingsProvider, SchemaComponent, useCompile } from '@nocobase/client';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { NAMESPACE } from './constants';
|
import { NAMESPACE } from './constants';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
const importFormSchema: ISchema = {
|
const importFormSchema: ISchema = {
|
||||||
@ -113,7 +121,6 @@ const importFormSchema: ISchema = {
|
|||||||
|
|
||||||
export const ImportAction = (props) => {
|
export const ImportAction = (props) => {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const { t } = useTranslation(NAMESPACE);
|
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
|
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
|
@ -17,7 +17,6 @@ import {
|
|||||||
useSchemaInitializerItem,
|
useSchemaInitializerItem,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NAMESPACE } from './constants';
|
|
||||||
import { useImportTranslation } from './locale';
|
import { useImportTranslation } from './locale';
|
||||||
import { useFields } from './useFields';
|
import { useFields } from './useFields';
|
||||||
import { Alert } from 'antd';
|
import { Alert } from 'antd';
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Action, OpenModeProvider, SchemaComponentOptions, useMobileLayout, usePopupSettings } from '@nocobase/client';
|
||||||
|
import { createGlobalStyle } from 'antd-style';
|
||||||
|
import React, { FC, useEffect } from 'react';
|
||||||
|
import { ActionDrawerUsedInMobile, useToAdaptActionDrawerToMobile } from './adaptor-of-desktop/ActionDrawer';
|
||||||
|
import { useToAdaptFilterActionToMobile } from './adaptor-of-desktop/FilterAction';
|
||||||
|
import { mobileComponents } from './pages/dynamic-page/MobilePage';
|
||||||
|
|
||||||
|
const GlobalStyle = createGlobalStyle`
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CommonDrawer: FC = (props) => {
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
||||||
|
|
||||||
|
// 在移动端布局中,只要弹窗是通过 URL 打开的,都需要显示成子页面的样子
|
||||||
|
if (isMobileLayout && isPopupVisibleControlledByURL()) {
|
||||||
|
return <Action.Page {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ActionDrawerUsedInMobile {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModeToComponent = {
|
||||||
|
page: Action.Page,
|
||||||
|
drawer: CommonDrawer,
|
||||||
|
modal: CommonDrawer,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MobileAdapter: FC = (props) => {
|
||||||
|
useToAdaptFilterActionToMobile();
|
||||||
|
useToAdaptActionDrawerToMobile();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.setProperty('--nb-mobile-page-tabs-content-padding', '12px');
|
||||||
|
document.body.style.setProperty('--nb-mobile-page-header-height', '46px');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GlobalStyle />
|
||||||
|
<OpenModeProvider defaultOpenMode="page" isMobile={true} openModeToComponent={openModeToComponent}>
|
||||||
|
<SchemaComponentOptions components={mobileComponents}>{props.children}</SchemaComponentOptions>
|
||||||
|
</OpenModeProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MobileComponentsProvider: FC = (props) => {
|
||||||
|
const { isMobileLayout } = useMobileLayout();
|
||||||
|
|
||||||
|
if (!isMobileLayout) {
|
||||||
|
return <>{props.children} </>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <MobileAdapter>{props.children}</MobileAdapter>;
|
||||||
|
};
|
@ -50,6 +50,23 @@ export const useMobileActionDrawerStyle = genStyleHook('nb-mobile-action-drawer'
|
|||||||
overflowX: 'hidden',
|
overflowX: 'hidden',
|
||||||
backgroundColor: token.colorBgLayout,
|
backgroundColor: token.colorBgLayout,
|
||||||
|
|
||||||
|
// 不带 tab 页的半窗
|
||||||
|
'& > .nb-grid-container': {
|
||||||
|
padding: `${token.paddingPageVertical}px ${token.paddingPageHorizontal}px`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 带有 tab 页的半窗
|
||||||
|
'.ant-tabs-nav': {
|
||||||
|
marginBottom: '0px !important',
|
||||||
|
padding: `0 ${token.paddingPageHorizontal + token.borderRadiusBlock / 2}px`,
|
||||||
|
backgroundColor: token.colorBgContainer,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 带有 tab 页的半窗
|
||||||
|
'.ant-tabs-content-holder': {
|
||||||
|
padding: `${token.paddingPageVertical}px ${token.paddingPageHorizontal}px`,
|
||||||
|
},
|
||||||
|
|
||||||
// clear the margin-bottom of the last block
|
// clear the margin-bottom of the last block
|
||||||
'& > .nb-grid-container > .nb-grid > .nb-grid-warp > .nb-grid-row:nth-last-child(2) .noco-card-item': {
|
'& > .nb-grid-container > .nb-grid > .nb-grid-warp > .nb-grid-row:nth-last-child(2) .noco-card-item': {
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
NocoBaseRecursionField,
|
NocoBaseRecursionField,
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
useActionContext,
|
useActionContext,
|
||||||
|
useGlobalTheme,
|
||||||
useZIndexContext,
|
useZIndexContext,
|
||||||
zIndexContext,
|
zIndexContext,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
@ -31,6 +32,7 @@ export const ActionDrawerUsedInMobile: any = observer((props: { footerNodeName?:
|
|||||||
const { popupContainerRef, visiblePopup } = usePopupContainer(visible);
|
const { popupContainerRef, visiblePopup } = usePopupContainer(visible);
|
||||||
const { componentCls, hashId } = useMobileActionDrawerStyle();
|
const { componentCls, hashId } = useMobileActionDrawerStyle();
|
||||||
const parentZIndex = useZIndexContext();
|
const parentZIndex = useZIndexContext();
|
||||||
|
const { theme: globalTheme } = useGlobalTheme();
|
||||||
|
|
||||||
// this schema need to add padding in the content area of the popup
|
// this schema need to add padding in the content area of the popup
|
||||||
const isSpecialSchema = isChangePasswordSchema(fieldSchema) || isEditProfileSchema(fieldSchema);
|
const isSpecialSchema = isChangePasswordSchema(fieldSchema) || isEditProfileSchema(fieldSchema);
|
||||||
@ -55,7 +57,6 @@ export const ActionDrawerUsedInMobile: any = observer((props: { footerNodeName?:
|
|||||||
});
|
});
|
||||||
|
|
||||||
const title = field.title || '';
|
const title = field.title || '';
|
||||||
const marginBlock = 18;
|
|
||||||
|
|
||||||
const closePopup = useCallback(() => {
|
const closePopup = useCallback(() => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
@ -63,14 +64,14 @@ export const ActionDrawerUsedInMobile: any = observer((props: { footerNodeName?:
|
|||||||
|
|
||||||
const theme = useMemo(() => {
|
const theme = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
|
...globalTheme,
|
||||||
token: {
|
token: {
|
||||||
marginBlock,
|
...globalTheme.token,
|
||||||
borderRadiusBlock: 0,
|
marginBlock: 12,
|
||||||
boxShadowTertiary: 'none',
|
|
||||||
zIndexPopupBase: newZIndex,
|
zIndexPopupBase: newZIndex,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [newZIndex]);
|
}, [globalTheme, newZIndex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<zIndexContext.Provider value={newZIndex}>
|
<zIndexContext.Provider value={newZIndex}>
|
||||||
|
@ -109,12 +109,12 @@ export const useToAdaptFilterActionToMobile = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 之所以不直接在 mobile-container 中设置 transform,是因为会影响到子页面区块的拖拽功能。详见:https://nocobase.height.app/T-4959
|
* 之所以不直接在 mobile-container 中设置 transform,是因为会影响到子页面区块的拖拽功能。
|
||||||
* @param visible
|
* @param visible
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const usePopupContainer = (visible: boolean) => {
|
export const usePopupContainer = (visible: boolean) => {
|
||||||
const [mobileContainer] = useState<HTMLElement>(() => document.querySelector('.mobile-container'));
|
const [mobileContainer] = useState<HTMLElement>(() => document.querySelector('.mobile-container') || document.body);
|
||||||
const [visiblePopup, setVisiblePopup] = useState(false);
|
const [visiblePopup, setVisiblePopup] = useState(false);
|
||||||
const popupContainerRef = React.useRef<HTMLDivElement>(null);
|
const popupContainerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const parentZIndex = useZIndexContext();
|
const parentZIndex = useZIndexContext();
|
||||||
|
@ -53,6 +53,7 @@ import { MobileSettingsBlockInitializer } from './mobile-blocks/settings-block/M
|
|||||||
import { MobileSettingsBlockSchemaSettings } from './mobile-blocks/settings-block/schemaSettings';
|
import { MobileSettingsBlockSchemaSettings } from './mobile-blocks/settings-block/schemaSettings';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import pkg from './../../package.json';
|
import pkg from './../../package.json';
|
||||||
|
import { MobileComponentsProvider } from './MobileComponentsProvider';
|
||||||
|
|
||||||
export * from './desktop-mode';
|
export * from './desktop-mode';
|
||||||
export * from './mobile';
|
export * from './mobile';
|
||||||
@ -104,6 +105,7 @@ export class PluginMobileClient extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
this.app.use(MobileComponentsProvider);
|
||||||
this.addComponents();
|
this.addComponents();
|
||||||
this.addAppRoutes();
|
this.addAppRoutes();
|
||||||
this.addRoutes();
|
this.addRoutes();
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Action,
|
|
||||||
AdminProvider,
|
AdminProvider,
|
||||||
AntdAppProvider,
|
AntdAppProvider,
|
||||||
AssociationFieldMode,
|
AssociationFieldMode,
|
||||||
@ -18,9 +17,10 @@ import {
|
|||||||
OpenModeProvider,
|
OpenModeProvider,
|
||||||
useAssociationFieldModeContext,
|
useAssociationFieldModeContext,
|
||||||
usePlugin,
|
usePlugin,
|
||||||
|
usePopupSettings,
|
||||||
zIndexContext,
|
zIndexContext,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import React from 'react';
|
import React, { FC } from 'react';
|
||||||
import { isDesktop } from 'react-device-detect';
|
import { isDesktop } from 'react-device-detect';
|
||||||
|
|
||||||
import { theme } from 'antd';
|
import { theme } from 'antd';
|
||||||
@ -36,10 +36,21 @@ import { PluginMobileClient } from '../index';
|
|||||||
import { MobileAppProvider } from './MobileAppContext';
|
import { MobileAppProvider } from './MobileAppContext';
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
|
|
||||||
|
const CommonDrawer: FC = (props) => {
|
||||||
|
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
||||||
|
|
||||||
|
// 在移动端布局中,只要弹窗是通过 URL 打开的,都需要显示成子页面的样子。因为这样可以通过左滑返回
|
||||||
|
if (isPopupVisibleControlledByURL()) {
|
||||||
|
return <MobileActionPage {...(props as any)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ActionDrawerUsedInMobile {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
const openModeToComponent = {
|
const openModeToComponent = {
|
||||||
page: MobileActionPage,
|
page: CommonDrawer,
|
||||||
drawer: ActionDrawerUsedInMobile,
|
drawer: ActionDrawerUsedInMobile,
|
||||||
modal: Action.Modal,
|
modal: ActionDrawerUsedInMobile,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Mobile = () => {
|
export const Mobile = () => {
|
||||||
|
@ -60,7 +60,7 @@ DatePickerMobile.FilterWithPicker = (props) => {
|
|||||||
};
|
};
|
||||||
DatePickerMobile.RangePicker = MobileRangePicker;
|
DatePickerMobile.RangePicker = MobileRangePicker;
|
||||||
|
|
||||||
const mobileComponents = {
|
export const mobileComponents = {
|
||||||
Button: MobileButton,
|
Button: MobileButton,
|
||||||
Select: (props) => {
|
Select: (props) => {
|
||||||
const { designable } = useDesignable();
|
const { designable } = useDesignable();
|
||||||
|
@ -285,5 +285,10 @@ const MobileTimePicker: ComposedMobileTimePicker = connect(
|
|||||||
mapReadPretty(NBTimePicker.ReadPretty),
|
mapReadPretty(NBTimePicker.ReadPretty),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
MobileDateTimePicker.displayName = 'MobileDateTimePicker';
|
||||||
|
MobileRangePicker.displayName = 'MobileRangePicker';
|
||||||
|
MobileDateFilterWithPicker.displayName = 'MobileDateFilterWithPicker';
|
||||||
|
MobileTimePicker.displayName = 'MobileTimePicker';
|
||||||
|
|
||||||
MobileTimePicker.RangePicker = NBTimePicker.RangePicker;
|
MobileTimePicker.RangePicker = NBTimePicker.RangePicker;
|
||||||
export { MobileDateTimePicker, MobileRangePicker, MobileDateFilterWithPicker, MobileTimePicker };
|
export { MobileDateTimePicker, MobileRangePicker, MobileDateFilterWithPicker, MobileTimePicker };
|
||||||
|
@ -108,4 +108,6 @@ const MobilePicker = connect(
|
|||||||
mapProps({ dataSource: 'options' }),
|
mapProps({ dataSource: 'options' }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
MobilePicker.displayName = 'MobilePicker';
|
||||||
|
|
||||||
export { MobilePicker };
|
export { MobilePicker };
|
||||||
|
@ -6,12 +6,12 @@
|
|||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
import { CheckCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { PageHeader } from '@ant-design/pro-layout';
|
||||||
|
import { Badge, Button, Layout, Menu, Tabs, Tooltip } from 'antd';
|
||||||
|
import classnames from 'classnames';
|
||||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, Outlet, useNavigate, useParams } from 'react-router-dom';
|
import { Link, Outlet, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Button, Layout, Menu, Badge, Tooltip, Tabs } from 'antd';
|
|
||||||
import { PageHeader } from '@ant-design/pro-layout';
|
|
||||||
import { CheckCircleOutlined } from '@ant-design/icons';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
@ -36,19 +36,6 @@ const layoutClass = css`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const sideClass = css`
|
|
||||||
overflow: auto;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.ant-layout-sider-children {
|
|
||||||
width: 200px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const contentClass = css`
|
const contentClass = css`
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
min-height: 280px;
|
min-height: 280px;
|
||||||
@ -134,8 +121,8 @@ function StatusTabs() {
|
|||||||
tabBarExtraContent={
|
tabBarExtraContent={
|
||||||
ExtraActions
|
ExtraActions
|
||||||
? {
|
? {
|
||||||
right: <ExtraActions />,
|
right: <ExtraActions />,
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -196,7 +183,7 @@ export function WorkflowTasks() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className={layoutClass}>
|
<Layout className={layoutClass}>
|
||||||
<Layout.Sider className={sideClass} theme="light">
|
<Layout.Sider theme="light" breakpoint="md" collapsedWidth="0" zeroWidthTriggerStyle={{ top: 38 }}>
|
||||||
<Menu mode="inline" selectedKeys={[typeKey]} items={items} style={{ height: '100%' }} />
|
<Menu mode="inline" selectedKeys={[typeKey]} items={items} style={{ height: '100%' }} />
|
||||||
</Layout.Sider>
|
</Layout.Sider>
|
||||||
<Layout
|
<Layout
|
||||||
|
Loading…
x
Reference in New Issue
Block a user