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:
parent
a6892dfff3
commit
4ac01b28db
@ -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]);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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>;
|
||||
};
|
||||
|
||||
|
@ -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 = () => {
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -1 +1,2 @@
|
||||
import './native-call';
|
||||
export * from './injects';
|
||||
|
@ -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;
|
||||
|
@ -0,0 +1 @@
|
||||
export * from './page';
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -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('../');
|
||||
},
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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({
|
||||
|
@ -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': {},
|
||||
|
@ -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) => {
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -26,6 +26,8 @@ const locale = {
|
||||
Settings: '设置',
|
||||
'Delete settings block': '删除设置区块',
|
||||
'Delete menu block': '删除菜单区块',
|
||||
'Mobile client access address': '移动端访问地址',
|
||||
'The full address is': '完整的地址是',
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user