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 redirect = location?.['query']?.redirect;
return useCallback(() => {
history.push(redirect || '/admin');
history.replace(redirect || '/admin');
}, [redirect, history]);
}

View File

@ -119,6 +119,7 @@ export const useFilterBlock = () => {
// 有可能存在页面没有提供 FilterBlockProvider 的情况,比如内部使用的数据表管理页面
if (!ctx) {
return {
inProvider: false,
recordDataBlocks: () => {},
getDataBlocks: () => [] as DataBlock[],
removeDataBlock: () => {},
@ -142,5 +143,13 @@ export const useFilterBlock = () => {
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 { TabPaneProps, Tabs as AntdTabs, TabsProps } from 'antd';
import classNames from 'classnames';
import React from 'react';
import React, { useMemo } from 'react';
import { Icon } from '../../../icon';
import { useSchemaInitializer } from '../../../schema-initializer';
import { DndContext, SortableItem } from '../../common';
import { useDesigner } from '../../hooks/useDesigner';
import { useTabsContext } from './context';
import { TabsContextProvider, useTabsContext } from './context';
import { TabsDesigner } from './Tabs.Designer';
export const Tabs: any = observer(
@ -15,6 +15,14 @@ export const Tabs: any = observer(
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
const contextProps = useTabsContext();
const PaneProvider = useMemo(() => {
if (contextProps.deep === false) {
return TabsContextProvider;
}
return React.Fragment;
}, [contextProps.deep]);
return (
<DndContext>
<AntdTabs
@ -27,7 +35,9 @@ export const Tabs: any = observer(
{fieldSchema.mapProperties((schema, key) => {
return (
<AntdTabs.TabPane tab={<RecursionField name={key} schema={schema} onlyRenderSelf />} key={key}>
<PaneProvider>
<RecursionField name={key} schema={schema} onlyRenderProperties />
</PaneProvider>
</AntdTabs.TabPane>
);
})}

View File

@ -1,9 +1,12 @@
import { TabsProps } from 'antd';
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>;
};

View File

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

View File

@ -1,5 +1,23 @@
import { Card, Form, Input } from 'antd';
import React from 'react';
import { useTranslation } from '../locale';
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';
export * from './injects';

View File

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

View File

@ -34,7 +34,11 @@ const InternalContainer: React.FC = (props) => {
const params = useParams<{ name: string }>();
const match = useRouteMatch();
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[]>(() => {
if (!redirectToUid) {

View File

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

View File

@ -14,6 +14,7 @@ import { useFieldSchema } from '@formily/react';
import { List } from 'antd-mobile';
import { useTranslation } from '../../../../locale';
import { menuItemSchema } from './schema';
import { PageSchema } from '../../common';
const InternalMenu: React.FC = (props) => {
const Designer = useDesigner();
@ -24,31 +25,7 @@ const InternalMenu: React.FC = (props) => {
const onAddMenuItem = (values: any) => {
const properties = {
page: {
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',
},
},
},
page: PageSchema,
};
return insertBeforeEnd({

View File

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

View File

@ -32,10 +32,11 @@ const InternalPage: React.FC = (props) => {
let hasGlobalActions = false;
if (!tabsSchema) {
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;
} else {
} else if (tabsSchema.properties) {
const schema = Object.values(tabsSchema.properties).sort((t1, t2) => t1['x-index'] - t2['x-index'])[0];
if (schema) {
history.replace({
pathname: location.pathname,
search: new URLSearchParams({
@ -43,6 +44,7 @@ const InternalPage: React.FC = (props) => {
}).toString(),
});
}
}
const onTabsChange = useCallback<TabsProps['onChange']>(
(key) => {
@ -110,7 +112,7 @@ const InternalPage: React.FC = (props) => {
return 'MHeader' === s['x-component'];
}}
></RecursionField>
<TabsContextProvider activeKey={query.get('tab')} onChange={onTabsChange}>
<TabsContextProvider deep={false} activeKey={query.get('tab')} onChange={onTabsChange}>
<RecursionField
schema={fieldSchema}
filterProperties={(s) => {

View File

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

View File

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

View File

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

View File

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

View File

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