fix(mobile-client): some mobile-client bugs (#2017)

* fix: avoid delete last tab will brake mobile client

* fix: when schema not exists

* feat: support redirect to mobile route in app

* fix: re-insert repeat routes

* feat: sync title

* fix: sync title

* fix: when TabBar is empty will cause page crashed

* fix: default label i18n not work

* fix: delete last menu will cannot add block ,Close T-434

* fix: tab cannot add block in drawer, Close T-437

* fix: hidden connect data block

* fix: login should use replace

* feat: display configuration of mobile-client address
This commit is contained in:
Dunqing 2023-06-13 11:31:34 +08:00 committed by GitHub
parent a6892dfff3
commit 4ac01b28db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 142 additions and 77 deletions

View File

@ -52,7 +52,7 @@ export function useRedirect(next = '/admin') {
const history = useHistory(); const history = useHistory();
const redirect = location?.['query']?.redirect; const redirect = location?.['query']?.redirect;
return useCallback(() => { return useCallback(() => {
history.push(redirect || '/admin'); history.replace(redirect || '/admin');
}, [redirect, history]); }, [redirect, history]);
} }

View File

@ -119,6 +119,7 @@ export const useFilterBlock = () => {
// 有可能存在页面没有提供 FilterBlockProvider 的情况,比如内部使用的数据表管理页面 // 有可能存在页面没有提供 FilterBlockProvider 的情况,比如内部使用的数据表管理页面
if (!ctx) { if (!ctx) {
return { return {
inProvider: false,
recordDataBlocks: () => {}, recordDataBlocks: () => {},
getDataBlocks: () => [] as DataBlock[], getDataBlocks: () => [] as DataBlock[],
removeDataBlock: () => {}, removeDataBlock: () => {},
@ -142,5 +143,13 @@ export const useFilterBlock = () => {
setDataBlocks((prev) => prev.filter((item) => item.uid !== uid)); setDataBlocks((prev) => prev.filter((item) => item.uid !== uid));
}; };
return { recordDataBlocks, getDataBlocks, removeDataBlock }; return {
recordDataBlocks,
getDataBlocks,
removeDataBlock,
/**
* running in FilterBlockProvider
*/
inProvider: true,
};
}; };

View File

@ -2,12 +2,12 @@ import { css } from '@emotion/css';
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { TabPaneProps, Tabs as AntdTabs, TabsProps } from 'antd'; import { TabPaneProps, Tabs as AntdTabs, TabsProps } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React, { useMemo } from 'react';
import { Icon } from '../../../icon'; import { Icon } from '../../../icon';
import { useSchemaInitializer } from '../../../schema-initializer'; import { useSchemaInitializer } from '../../../schema-initializer';
import { DndContext, SortableItem } from '../../common'; import { DndContext, SortableItem } from '../../common';
import { useDesigner } from '../../hooks/useDesigner'; import { useDesigner } from '../../hooks/useDesigner';
import { useTabsContext } from './context'; import { TabsContextProvider, useTabsContext } from './context';
import { TabsDesigner } from './Tabs.Designer'; import { TabsDesigner } from './Tabs.Designer';
export const Tabs: any = observer( export const Tabs: any = observer(
@ -15,6 +15,14 @@ export const Tabs: any = observer(
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializer(fieldSchema['x-initializer']); const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
const contextProps = useTabsContext(); const contextProps = useTabsContext();
const PaneProvider = useMemo(() => {
if (contextProps.deep === false) {
return TabsContextProvider;
}
return React.Fragment;
}, [contextProps.deep]);
return ( return (
<DndContext> <DndContext>
<AntdTabs <AntdTabs
@ -27,7 +35,9 @@ export const Tabs: any = observer(
{fieldSchema.mapProperties((schema, key) => { {fieldSchema.mapProperties((schema, key) => {
return ( return (
<AntdTabs.TabPane tab={<RecursionField name={key} schema={schema} onlyRenderSelf />} key={key}> <AntdTabs.TabPane tab={<RecursionField name={key} schema={schema} onlyRenderSelf />} key={key}>
<RecursionField name={key} schema={schema} onlyRenderProperties /> <PaneProvider>
<RecursionField name={key} schema={schema} onlyRenderProperties />
</PaneProvider>
</AntdTabs.TabPane> </AntdTabs.TabPane>
); );
})} })}

View File

@ -1,9 +1,12 @@
import { TabsProps } from 'antd'; import { TabsProps } from 'antd';
import React from 'react'; import React from 'react';
const TabsContext = React.createContext<TabsProps>({}); interface TabsContextProps extends TabsProps {
deep?: boolean;
}
const TabsContext = React.createContext<TabsContextProps>({});
export const TabsContextProvider: React.FC<TabsProps> = ({ children, ...props }) => { export const TabsContextProvider: React.FC<TabsContextProps> = ({ children, ...props }) => {
return <TabsContext.Provider value={props}>{children}</TabsContext.Provider>; return <TabsContext.Provider value={props}>{children}</TabsContext.Provider>;
}; };

View File

@ -41,6 +41,7 @@ import {
useCollectionManager, useCollectionManager,
useCompile, useCompile,
useDesignable, useDesignable,
useFilterBlock,
useLinkageCollectionFilterOptions, useLinkageCollectionFilterOptions,
} from '..'; } from '..';
import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks'; import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks';
@ -465,10 +466,15 @@ SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: {
const { dn } = useDesignable(); const { dn } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const collection = useCollection(); const collection = useCollection();
const { inProvider } = useFilterBlock();
const dataBlocks = useSupportedBlocks(type); const dataBlocks = useSupportedBlocks(type);
let { targets = [], uid } = findFilterTargets(fieldSchema); let { targets = [], uid } = findFilterTargets(fieldSchema);
const compile = useCompile(); const compile = useCompile();
if (!inProvider) {
return null;
}
const Content = dataBlocks.map((block) => { const Content = dataBlocks.map((block) => {
const title = `${compile(block.collection.title)} #${block.uid.slice(0, 4)}`; const title = `${compile(block.collection.title)} #${block.uid.slice(0, 4)}`;
const onHover = () => { const onHover = () => {

View File

@ -1,5 +1,23 @@
import { Card, Form, Input } from 'antd';
import React from 'react'; import React from 'react';
import { useTranslation } from '../locale';
export const AppConfiguration = () => { export const AppConfiguration = () => {
return <>App Configuration</>; const { t } = useTranslation();
return (
<Card
style={{
minHeight: '600px',
}}
>
<Form layout="vertical">
<Form.Item
tooltip={`${t('The full address is')} ${window.origin}/mobile`}
label={t('Mobile client access address')}
>
<Input value="/mobile" disabled />
</Form.Item>
</Form>
</Card>
);
}; };

View File

@ -1 +1,2 @@
import './native-call'; import './native-call';
export * from './injects';

View File

@ -10,3 +10,5 @@ const jsBridge = (window as any).jsBridge as {
export const invoke: InvokeFunction = (params, cb) => { export const invoke: InvokeFunction = (params, cb) => {
jsBridge.invoke(params, cb); jsBridge.invoke(params, cb);
}; };
export const isJSBridge = !!jsBridge;

View File

@ -0,0 +1 @@
export * from './page';

View File

@ -0,0 +1,16 @@
export const PageSchema = {
type: 'void',
'x-component': 'MPage',
'x-designer': 'MPage.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'MBlockInitializers',
'x-component-props': {
showDivider: false,
},
},
},
};

View File

@ -1,12 +1,13 @@
import { SchemaSettings, useDesignable } from '@nocobase/client'; import { SchemaSettings, useDesignable } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useTranslation } from '../../../../locale'; import { generateNTemplate, useTranslation } from '../../../../locale';
import { Schema, useField, useFieldSchema } from '@formily/react'; import { Schema, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { findSchema } from '../../helpers'; import { findSchema } from '../../helpers';
import { Button } from 'antd'; import { Button } from 'antd';
import { MenuOutlined } from '@ant-design/icons'; import { MenuOutlined } from '@ant-design/icons';
import { PageSchema } from '../../common';
export const ContainerDesigner = () => { export const ContainerDesigner = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -60,7 +61,7 @@ export const ContainerDesigner = () => {
'x-designer': 'MTabBar.Item.Designer', 'x-designer': 'MTabBar.Item.Designer',
'x-component-props': { 'x-component-props': {
icon: 'HomeOutlined', icon: 'HomeOutlined',
title: t('Untitled'), title: generateNTemplate('Untitled'),
}, },
properties: { properties: {
page: pageSchema.toJSON(), page: pageSchema.toJSON(),
@ -69,10 +70,12 @@ export const ContainerDesigner = () => {
}, },
}); });
} else { } else {
const pageSchema = findSchema(tabBarSchema.properties[Object.keys(tabBarSchema.properties)[0]], 'MPage'); const tabBarSchemaFirstKey = Object.keys(tabBarSchema.properties || {})?.[0];
if (!pageSchema) return; const pageSchema = tabBarSchemaFirstKey
? findSchema(tabBarSchema.properties[tabBarSchemaFirstKey], 'MPage')
: null;
await dn.remove(tabBarSchema); await dn.remove(tabBarSchema);
await dn.insertBeforeEnd(pageSchema, { await dn.insertBeforeEnd(pageSchema || PageSchema, {
onSuccess() { onSuccess() {
history.push('../'); history.push('../');
}, },

View File

@ -34,7 +34,11 @@ const InternalContainer: React.FC = (props) => {
const params = useParams<{ name: string }>(); const params = useParams<{ name: string }>();
const match = useRouteMatch(); const match = useRouteMatch();
const tabBarSchema = fieldSchema?.properties?.['tabBar']; const tabBarSchema = fieldSchema?.properties?.['tabBar'];
const redirectToUid = tabBarSchema?.properties[Object.keys(tabBarSchema.properties)[0]]['x-uid']; const tabBarCurrentFirstKey = tabBarSchema?.properties ? Object.keys(tabBarSchema.properties)[0] : null;
let redirectToUid = null;
if (tabBarCurrentFirstKey) {
redirectToUid = tabBarSchema?.properties[tabBarCurrentFirstKey]?.['x-uid'];
}
const tabRoutes = useMemo<RouteProps[]>(() => { const tabRoutes = useMemo<RouteProps[]>(() => {
if (!redirectToUid) { if (!redirectToUid) {

View File

@ -1,10 +1,11 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { SortableItem, useCompile, useDesigner } from '@nocobase/client'; import { SortableItem, useCompile, useDesigner } from '@nocobase/client';
import { NavBar, NavBarProps } from 'antd-mobile'; import { NavBar, NavBarProps } from 'antd-mobile';
import React from 'react'; import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { HeaderDesigner } from './Header.Designer'; import { HeaderDesigner } from './Header.Designer';
import { useField, useFieldSchema } from '@formily/react'; import { useField } from '@formily/react';
import { generateNTemplate } from '../../../../locale';
export interface HeaderProps extends NavBarProps { export interface HeaderProps extends NavBarProps {
title?: string; title?: string;
@ -12,10 +13,17 @@ export interface HeaderProps extends NavBarProps {
} }
const InternalHeader = (props: HeaderProps) => { const InternalHeader = (props: HeaderProps) => {
const field = useField(); const field = useField();
const { title = '{{ t("Untitled") }}', showBack = false } = { ...props, ...field?.componentProps }; const { title = generateNTemplate('Untitled'), showBack = false } = { ...props, ...field?.componentProps };
const Designer = useDesigner(); const Designer = useDesigner();
const compile = useCompile(); const compile = useCompile();
const compiledTitle = compile(title);
const history = useHistory(); const history = useHistory();
useEffect(() => {
// sync title
document.title = `${compiledTitle} - NocoBase`;
}, [compiledTitle]);
return ( return (
<SortableItem <SortableItem
className={cx( className={cx(
@ -27,7 +35,7 @@ const InternalHeader = (props: HeaderProps) => {
)} )}
> >
<NavBar backArrow={showBack} onBack={history.goBack}> <NavBar backArrow={showBack} onBack={history.goBack}>
{compile(title)} {compiledTitle}
</NavBar> </NavBar>
<Designer /> <Designer />
</SortableItem> </SortableItem>

View File

@ -14,6 +14,7 @@ import { useFieldSchema } from '@formily/react';
import { List } from 'antd-mobile'; import { List } from 'antd-mobile';
import { useTranslation } from '../../../../locale'; import { useTranslation } from '../../../../locale';
import { menuItemSchema } from './schema'; import { menuItemSchema } from './schema';
import { PageSchema } from '../../common';
const InternalMenu: React.FC = (props) => { const InternalMenu: React.FC = (props) => {
const Designer = useDesigner(); const Designer = useDesigner();
@ -24,31 +25,7 @@ const InternalMenu: React.FC = (props) => {
const onAddMenuItem = (values: any) => { const onAddMenuItem = (values: any) => {
const properties = { const properties = {
page: { page: PageSchema,
type: 'void',
'x-component': 'MPage',
'x-designer': 'MPage.Designer',
'x-component-props': {},
properties: {
header: {
type: 'void',
'x-component': 'MHeader',
'x-designer': 'MHeader.Designer',
'x-component-props': {
title: values.name,
showBack: true,
},
},
grid: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
showDivider: false,
},
'x-initializer': 'MBlockInitializers',
},
},
},
}; };
return insertBeforeEnd({ return insertBeforeEnd({

View File

@ -1,6 +1,6 @@
import { GeneralSchemaDesigner, SchemaSettings, useDesignable } from '@nocobase/client'; import { GeneralSchemaDesigner, SchemaSettings, useDesignable } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useTranslation } from '../../../../locale'; import { generateNTemplate, useTranslation } from '../../../../locale';
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import { findGridSchema } from '../../helpers'; import { findGridSchema } from '../../helpers';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
@ -78,7 +78,7 @@ export const PageDesigner = (props) => {
properties: { properties: {
tab1: { tab1: {
type: 'void', type: 'void',
title: '{{t("Untitled")}}', title: generateNTemplate('Untitled'),
'x-component': 'Tabs.TabPane', 'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer', 'x-designer': 'Tabs.Designer',
'x-component-props': {}, 'x-component-props': {},

View File

@ -32,16 +32,18 @@ const InternalPage: React.FC = (props) => {
let hasGlobalActions = false; let hasGlobalActions = false;
if (!tabsSchema) { if (!tabsSchema) {
hasGlobalActions = countGridCol(fieldSchema.properties['grid'], 2) === 1; hasGlobalActions = countGridCol(fieldSchema.properties['grid'], 2) === 1;
} else if (query.has('tab') && tabsSchema.properties[query.get('tab')]) { } else if (query.has('tab') && tabsSchema.properties?.[query.get('tab')]) {
hasGlobalActions = countGridCol(tabsSchema.properties[query.get('tab')]?.properties?.['grid'], 2) === 1; hasGlobalActions = countGridCol(tabsSchema.properties[query.get('tab')]?.properties?.['grid'], 2) === 1;
} else { } else if (tabsSchema.properties) {
const schema = Object.values(tabsSchema.properties).sort((t1, t2) => t1['x-index'] - t2['x-index'])[0]; const schema = Object.values(tabsSchema.properties).sort((t1, t2) => t1['x-index'] - t2['x-index'])[0];
history.replace({ if (schema) {
pathname: location.pathname, history.replace({
search: new URLSearchParams({ pathname: location.pathname,
tab: schema.name.toString(), search: new URLSearchParams({
}).toString(), tab: schema.name.toString(),
}); }).toString(),
});
}
} }
const onTabsChange = useCallback<TabsProps['onChange']>( const onTabsChange = useCallback<TabsProps['onChange']>(
@ -110,7 +112,7 @@ const InternalPage: React.FC = (props) => {
return 'MHeader' === s['x-component']; return 'MHeader' === s['x-component'];
}} }}
></RecursionField> ></RecursionField>
<TabsContextProvider activeKey={query.get('tab')} onChange={onTabsChange}> <TabsContextProvider deep={false} activeKey={query.get('tab')} onChange={onTabsChange}>
<RecursionField <RecursionField
schema={fieldSchema} schema={fieldSchema}
filterProperties={(s) => { filterProperties={(s) => {

View File

@ -2,21 +2,30 @@ import { TabBar } from 'antd-mobile';
import { TabBarItem } from './TabBar.Item'; import { TabBarItem } from './TabBar.Item';
import React, { useCallback, useContext } from 'react'; import React, { useCallback, useContext } from 'react';
import { SchemaOptionsContext, useFieldSchema } from '@formily/react'; import { SchemaOptionsContext, useFieldSchema } from '@formily/react';
import { DndContext, Icon, SchemaComponent, SchemaInitializer, SortableItem, useDesignable } from '@nocobase/client'; import {
DndContext,
Icon,
SchemaComponent,
SchemaInitializer,
SortableItem,
useCompile,
useDesignable,
} from '@nocobase/client';
import { useTranslation } from '../../../../locale'; import { useTranslation } from '../../../../locale';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import { tabItemSchema } from './schema'; import { tabItemSchema } from './schema';
import { PageSchema } from '../../common';
export const InternalTabBar: React.FC = (props) => { export const InternalTabBar: React.FC = (props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { designable } = useDesignable(); const { designable } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const options = useContext(SchemaOptionsContext); const { insertBeforeEnd } = useDesignable();
const { insertBeforeEnd, dn } = useDesignable();
const history = useHistory(); const history = useHistory();
const params = useParams<{ name: string }>(); const params = useParams<{ name: string }>();
const compile = useCompile();
const onAddTab = useCallback((values: any) => { const onAddTab = useCallback((values: any) => {
return insertBeforeEnd({ return insertBeforeEnd({
@ -25,25 +34,11 @@ export const InternalTabBar: React.FC = (props) => {
'x-component-props': values, 'x-component-props': values,
'x-designer': 'MTabBar.Item.Designer', 'x-designer': 'MTabBar.Item.Designer',
properties: { properties: {
[uid()]: { [uid()]: PageSchema,
type: 'void',
'x-component': 'MPage',
'x-designer': 'MPage.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'MBlockInitializers',
'x-component-props': {
showDivider: false,
},
},
},
},
}, },
}); });
}, []); }, []);
return ( return (
<SortableItem <SortableItem
className={cx( className={cx(
@ -80,7 +75,7 @@ export const InternalTabBar: React.FC = (props) => {
key={`tab_${schema['x-uid']}`} key={`tab_${schema['x-uid']}`}
title={ title={
<> <>
{cp.title} {compile(cp.title)}
<SchemaComponent schema={schema} name={name} /> <SchemaComponent schema={schema} name={name} />
</> </>
} }
@ -88,7 +83,7 @@ export const InternalTabBar: React.FC = (props) => {
></TabBar.Item> ></TabBar.Item>
); );
})} })}
{designable && Object.keys(fieldSchema.properties).length < 5 ? ( {designable && (!fieldSchema.properties || Object.keys(fieldSchema.properties).length < 5) ? (
<TabBar.Item <TabBar.Item
className={css` className={css`
.adm-tab-bar-item-icon { .adm-tab-bar-item-icon {

View File

@ -13,7 +13,7 @@ export function lang(key: string) {
} }
export function generateNTemplate(key: string) { export function generateNTemplate(key: string) {
return `{{t('${key}', { ns: '${NAMESPACE}' })}}`; return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
} }
export function useTranslation() { export function useTranslation() {

View File

@ -26,6 +26,8 @@ const locale = {
Settings: '设置', Settings: '设置',
'Delete settings block': '删除设置区块', 'Delete settings block': '删除设置区块',
'Delete menu block': '删除菜单区块', 'Delete menu block': '删除菜单区块',
'Mobile client access address': '移动端访问地址',
'The full address is': '完整的地址是',
}; };
export default locale; export default locale;

View File

@ -1,5 +1,5 @@
import { RouteSwitch, useRoutes } from '@nocobase/client'; import { RouteSwitch, useRoutes } from '@nocobase/client';
import React, { useEffect, useMemo } from 'react'; import React, { useMemo } from 'react';
import { HashRouter } from 'react-router-dom'; import { HashRouter } from 'react-router-dom';
import { InterfaceProvider } from './InterfaceProvider'; import { InterfaceProvider } from './InterfaceProvider';

View File

@ -1,9 +1,17 @@
import { RouteSwitchProvider, useRouteSwitchContext } from '@nocobase/client'; import { RouteSwitchProvider, useRouteSwitchContext } from '@nocobase/client';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import MApplication from './Application'; import MApplication from './Application';
import { isJSBridge } from '../core/bridge';
export const RouterSwitchProvider = (props) => { export const RouterSwitchProvider = (props) => {
const { routes, components } = useRouteSwitchContext(); const { routes, components } = useRouteSwitchContext();
// redirect to mobile
if (isJSBridge) {
const redirectRoute = routes[0];
if (redirectRoute.type === 'redirect' && redirectRoute.to === '/admin') {
redirectRoute.to = '/mobile';
}
}
return ( return (
<RouteSwitchProvider <RouteSwitchProvider
routes={routes} routes={routes}