@nocobase/client-v2 2.2.0-alpha.4 → 2.2.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/es/BaseApplication.d.ts +2 -0
  2. package/es/RouterManager.d.ts +15 -0
  3. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  4. package/es/entry-actions/index.d.ts +9 -0
  5. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  6. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  7. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  8. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  9. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  10. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  11. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  12. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  13. package/es/index.d.ts +4 -0
  14. package/es/index.mjs +178 -124
  15. package/es/utils/markdownSanitize.d.ts +11 -0
  16. package/lib/index.js +164 -110
  17. package/package.json +7 -7
  18. package/src/BaseApplication.tsx +2 -0
  19. package/src/RouterManager.tsx +142 -1
  20. package/src/__tests__/RouterManager.test.ts +125 -0
  21. package/src/entry-actions/EntryActionManager.ts +76 -0
  22. package/src/entry-actions/index.ts +10 -0
  23. package/src/flow/FlowPage.tsx +29 -2
  24. package/src/flow/__tests__/FlowPage.test.tsx +152 -25
  25. package/src/flow/__tests__/FlowRoute.test.tsx +547 -2
  26. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
  27. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
  28. package/src/flow/actions/dataScopeFilter.ts +10 -1
  29. package/src/flow/actions/openView.tsx +21 -1
  30. package/src/flow/admin-shell/BaseLayoutModel.tsx +111 -0
  31. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
  32. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
  33. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -4
  34. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  35. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  36. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +359 -2
  37. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
  38. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  39. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  40. package/src/flow/common/Markdown/Display.tsx +14 -3
  41. package/src/flow/common/Markdown/Edit.tsx +19 -7
  42. package/src/flow/components/FlowRoute.tsx +128 -16
  43. package/src/flow/internal/components/Markdown/util.ts +2 -1
  44. package/src/flow/models/base/ActionModel.tsx +10 -8
  45. package/src/flow/models/base/ActionModelCore.tsx +5 -0
  46. package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
  47. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
  48. package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
  49. package/src/flow/routeTransientInputArgs.ts +27 -0
  50. package/src/index.ts +4 -0
  51. package/src/nocobase-buildin-plugin/index.tsx +7 -1
  52. package/src/utils/markdownSanitize.ts +88 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.2.0-alpha.4",
3
+ "version": "2.2.0-alpha.5",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -27,11 +27,11 @@
27
27
  "@formily/antd-v5": "1.2.3",
28
28
  "@formily/react": "^2.2.27",
29
29
  "@formily/shared": "^2.2.27",
30
- "@nocobase/evaluators": "2.2.0-alpha.4",
31
- "@nocobase/flow-engine": "2.2.0-alpha.4",
32
- "@nocobase/sdk": "2.2.0-alpha.4",
33
- "@nocobase/shared": "2.2.0-alpha.4",
34
- "@nocobase/utils": "2.2.0-alpha.4",
30
+ "@nocobase/evaluators": "2.2.0-alpha.5",
31
+ "@nocobase/flow-engine": "2.2.0-alpha.5",
32
+ "@nocobase/sdk": "2.2.0-alpha.5",
33
+ "@nocobase/shared": "2.2.0-alpha.5",
34
+ "@nocobase/utils": "2.2.0-alpha.5",
35
35
  "ahooks": "^3.7.2",
36
36
  "antd": "5.24.2",
37
37
  "antd-style": "3.7.1",
@@ -46,5 +46,5 @@
46
46
  "react-i18next": "^11.15.1",
47
47
  "react-router-dom": "^6.30.1"
48
48
  },
49
- "gitHead": "81bf8733db4bccf08a6bc5e6d97f274bce349161"
49
+ "gitHead": "814ff497dd1233ca7218c9f7f2a016c2ead6516d"
50
50
  }
@@ -30,6 +30,7 @@ import AntdAppProvider from './theme/AntdAppProvider';
30
30
  import { GlobalThemeProvider } from './theme';
31
31
  import { AIManager } from './ai';
32
32
  import { AppError, AppMaintaining, AppMaintainingDialog, AppNotFound, AppSpin, BlankComponent } from './components';
33
+ import { EntryActionManager } from './entry-actions';
33
34
  import { SystemSettingsSource } from './flow/system-settings';
34
35
  import { LayoutManager } from './layout-manager/LayoutManager';
35
36
  import type { PluginClass, PluginManager, PluginType } from './PluginManager';
@@ -126,6 +127,7 @@ export abstract class BaseApplication<
126
127
  public pluginManager: TPluginManager;
127
128
  public pluginSettingsManager: TPluginSettingsManager;
128
129
  public layoutManager: LayoutManager<this>;
130
+ public entryActionManager = new EntryActionManager();
129
131
  public aiManager!: AIManager;
130
132
  public devDynamicImport?: DevDynamicImport;
131
133
  public requirejs!: RequireJS;
@@ -26,6 +26,7 @@ import type { BaseApplication } from './BaseApplication';
26
26
  import { BlankComponent, RouterContextCleaner } from './components';
27
27
  import { RouterBridge } from './components/RouterBridge';
28
28
  import { Router } from '@remix-run/router';
29
+ import { getV2EffectiveBasePath } from './authRedirect';
29
30
 
30
31
  export interface BrowserRouterOptions extends Omit<BrowserRouterProps, 'children'> {
31
32
  type?: 'browser';
@@ -55,6 +56,87 @@ export interface RouteType extends Omit<RouteObject, 'children' | 'Component'> {
55
56
  export type RenderComponentType = (Component: ComponentTypeAndString, props?: any) => React.ReactNode;
56
57
  export type RouterComponentType = React.FC<{ BaseLayout?: ComponentType }>;
57
58
 
59
+ const DEFAULT_ADMIN_ROUTE_PATH = '/admin';
60
+
61
+ type AdminRouteNavigationTarget = {
62
+ currentPathname?: string;
63
+ targetPathname: unknown;
64
+ basePath?: string;
65
+ adminRoutePath?: string;
66
+ replace?: boolean;
67
+ };
68
+
69
+ function trimPathSearchAndHash(pathname: string) {
70
+ return pathname.split(/[?#]/)[0];
71
+ }
72
+
73
+ function splitPathSearchAndHash(pathname: string) {
74
+ const match = pathname.match(/^([^?#]*)(.*)$/);
75
+ return {
76
+ pathname: match?.[1] || '',
77
+ suffix: match?.[2] || '',
78
+ };
79
+ }
80
+
81
+ function normalizeRootPath(pathname?: string) {
82
+ const trimmed = trimPathSearchAndHash(pathname || '').trim();
83
+ if (!trimmed || trimmed === '/') {
84
+ return '/';
85
+ }
86
+ return `/${trimmed.replace(/^\/+|\/+$/g, '')}`;
87
+ }
88
+
89
+ function isRootRelativePath(pathname: string) {
90
+ return pathname.startsWith('/') && !pathname.startsWith('//') && !pathname.startsWith('/\\');
91
+ }
92
+
93
+ function removeBasePath(pathname: string, basePath?: string) {
94
+ const normalizedPathname = normalizeRootPath(pathname);
95
+ const normalizedBasePath = normalizeRootPath(basePath);
96
+
97
+ if (normalizedBasePath === '/') {
98
+ return normalizedPathname;
99
+ }
100
+ if (normalizedPathname === normalizedBasePath) {
101
+ return '/';
102
+ }
103
+ if (normalizedPathname.startsWith(`${normalizedBasePath}/`)) {
104
+ return normalizeRootPath(normalizedPathname.slice(normalizedBasePath.length));
105
+ }
106
+ return normalizedPathname;
107
+ }
108
+
109
+ function removeAppSegment(pathname: string) {
110
+ return normalizeRootPath(pathname).replace(/^\/(?:apps|_app)\/[^/]+(?=\/|$)/, '') || '/';
111
+ }
112
+
113
+ function normalizePortalRoutePath(pathname: string, basePath?: string) {
114
+ return removeAppSegment(removeBasePath(pathname, basePath));
115
+ }
116
+
117
+ function isSameOrChildPath(pathname: string, basePath: string) {
118
+ const normalizedPathname = normalizeRootPath(pathname);
119
+ const normalizedBasePath = normalizeRootPath(basePath);
120
+ return normalizedPathname === normalizedBasePath || normalizedPathname.startsWith(`${normalizedBasePath}/`);
121
+ }
122
+
123
+ export function shouldOpenAdminRouteInNewWindow(options: AdminRouteNavigationTarget) {
124
+ if (options.replace || typeof options.targetPathname !== 'string') {
125
+ return false;
126
+ }
127
+
128
+ const targetPathname = options.targetPathname.trim();
129
+ if (!isRootRelativePath(targetPathname)) {
130
+ return false;
131
+ }
132
+
133
+ const adminRoutePath = normalizeRootPath(options.adminRoutePath || DEFAULT_ADMIN_ROUTE_PATH);
134
+ const currentRoutePath = normalizePortalRoutePath(options.currentPathname || '/', options.basePath);
135
+ const targetRoutePath = normalizePortalRoutePath(targetPathname, options.basePath);
136
+
137
+ return !isSameOrChildPath(currentRoutePath, adminRoutePath) && isSameOrChildPath(targetRoutePath, adminRoutePath);
138
+ }
139
+
58
140
  function removeBasename(pathname: string, basename?: string) {
59
141
  if (!basename || basename === '/') {
60
142
  return pathname;
@@ -72,6 +154,7 @@ function removeBasename(pathname: string, basename?: string) {
72
154
  export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<any>> {
73
155
  protected routes: Record<string, RouteType> = {};
74
156
  protected options: RouterOptions;
157
+ private routerNavigate?: Router['navigate'];
75
158
  public app: TApp;
76
159
  public router!: Router;
77
160
  get basename() {
@@ -81,7 +164,7 @@ export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<a
81
164
  return this.router.state;
82
165
  }
83
166
  get navigate() {
84
- return this.router.navigate;
167
+ return this.navigateWithPortalPolicy;
85
168
  }
86
169
 
87
170
  constructor(options: RouterOptions = {}, app: TApp) {
@@ -90,6 +173,62 @@ export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<a
90
173
  this.routes = options.routes || {};
91
174
  }
92
175
 
176
+ private navigateWithPortalPolicy: Router['navigate'] = ((to, opts) => {
177
+ if (this.shouldOpenAdminRouteInNewWindow(to, opts)) {
178
+ window.open(this.getAdminRouteNavigationHref(to as string), '_blank', 'noopener,noreferrer');
179
+ return Promise.resolve();
180
+ }
181
+
182
+ const navigate = this.routerNavigate || this.router.navigate.bind(this.router);
183
+ return navigate(to, opts);
184
+ }) as Router['navigate'];
185
+
186
+ private shouldOpenAdminRouteInNewWindow(
187
+ to: Parameters<Router['navigate']>[0],
188
+ opts?: Parameters<Router['navigate']>[1],
189
+ ) {
190
+ if (this.options.type && this.options.type !== 'browser') {
191
+ return false;
192
+ }
193
+
194
+ return shouldOpenAdminRouteInNewWindow({
195
+ currentPathname:
196
+ typeof window !== 'undefined' ? window.location.pathname : this.router?.state?.location?.pathname,
197
+ targetPathname: to,
198
+ basePath: this.getRuntimeBasePath(),
199
+ adminRoutePath: this.getAdminRoutePath(),
200
+ replace: opts?.replace,
201
+ });
202
+ }
203
+
204
+ private getRuntimeBasePath() {
205
+ return getV2EffectiveBasePath(this.app);
206
+ }
207
+
208
+ private getAdminRoutePath() {
209
+ try {
210
+ return this.app.layoutManager?.getLayout?.('admin')?.routePath || DEFAULT_ADMIN_ROUTE_PATH;
211
+ } catch {
212
+ return DEFAULT_ADMIN_ROUTE_PATH;
213
+ }
214
+ }
215
+
216
+ private getAdminRouteNavigationHref(pathname: string) {
217
+ const { pathname: rawPathname, suffix } = splitPathSearchAndHash(pathname.trim());
218
+ const normalizedPathname = normalizeRootPath(rawPathname);
219
+ const runtimeBasePath = this.getRuntimeBasePath();
220
+ const normalizedBasePath = normalizeRootPath(runtimeBasePath);
221
+
222
+ if (
223
+ normalizedBasePath !== '/' &&
224
+ (normalizedPathname === normalizedBasePath || normalizedPathname.startsWith(`${normalizedBasePath}/`))
225
+ ) {
226
+ return `${normalizedPathname}${suffix}`;
227
+ }
228
+
229
+ return `${this.app.getHref(normalizePortalRoutePath(normalizedPathname, runtimeBasePath))}${suffix}`;
230
+ }
231
+
93
232
  protected resolveLoadedComponent(moduleOrComponent: ComponentLoaderResult): ComponentTypeAndString | undefined {
94
233
  if (!moduleOrComponent) {
95
234
  return undefined;
@@ -257,6 +396,8 @@ export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<a
257
396
  ],
258
397
  opts,
259
398
  );
399
+ this.routerNavigate = this.router.navigate.bind(this.router) as Router['navigate'];
400
+ this.router.navigate = this.navigateWithPortalPolicy;
260
401
 
261
402
  const RenderRouter: RouterComponentType = ({ BaseLayout = BlankComponent }) => {
262
403
  return (
@@ -0,0 +1,125 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import type { BaseApplication } from '../BaseApplication';
11
+ import { RouterManager, shouldOpenAdminRouteInNewWindow } from '../RouterManager';
12
+
13
+ describe('RouterManager', () => {
14
+ describe('shouldOpenAdminRouteInNewWindow', () => {
15
+ it('opens admin route in a new window from a non-admin portal', () => {
16
+ expect(
17
+ shouldOpenAdminRouteInNewWindow({
18
+ currentPathname: '/v/test',
19
+ targetPathname: '/admin/settings',
20
+ basePath: '/v',
21
+ adminRoutePath: '/admin',
22
+ }),
23
+ ).toBe(true);
24
+ });
25
+
26
+ it('supports target pathname with the v2 base path', () => {
27
+ expect(
28
+ shouldOpenAdminRouteInNewWindow({
29
+ currentPathname: '/nocobase/v/test',
30
+ targetPathname: '/nocobase/v/admin/settings',
31
+ basePath: '/nocobase/v',
32
+ adminRoutePath: '/admin',
33
+ }),
34
+ ).toBe(true);
35
+ });
36
+
37
+ it('supports custom portal routes in sub-apps', () => {
38
+ expect(
39
+ shouldOpenAdminRouteInNewWindow({
40
+ currentPathname: '/v/apps/a_9xlild35jir/crm-amd/ekeisumx1zu',
41
+ targetPathname: '/v/apps/a_9xlild35jir/admin/settings',
42
+ basePath: '/v',
43
+ adminRoutePath: '/admin',
44
+ }),
45
+ ).toBe(true);
46
+ });
47
+
48
+ it('keeps admin route navigation in the current window when already in sub-app admin', () => {
49
+ expect(
50
+ shouldOpenAdminRouteInNewWindow({
51
+ currentPathname: '/v/apps/a_9xlild35jir/admin',
52
+ targetPathname: '/v/apps/a_9xlild35jir/admin/settings',
53
+ basePath: '/v',
54
+ adminRoutePath: '/admin',
55
+ }),
56
+ ).toBe(false);
57
+ });
58
+
59
+ it('keeps admin route navigation in the current window when already in admin', () => {
60
+ expect(
61
+ shouldOpenAdminRouteInNewWindow({
62
+ currentPathname: '/v/admin',
63
+ targetPathname: '/admin/settings',
64
+ basePath: '/v',
65
+ adminRoutePath: '/admin',
66
+ }),
67
+ ).toBe(false);
68
+ });
69
+
70
+ it('keeps non-admin portal navigation in the current window', () => {
71
+ expect(
72
+ shouldOpenAdminRouteInNewWindow({
73
+ currentPathname: '/v/test',
74
+ targetPathname: '/test/page',
75
+ basePath: '/v',
76
+ adminRoutePath: '/admin',
77
+ }),
78
+ ).toBe(false);
79
+ });
80
+
81
+ it('does not intercept replace navigation or relative paths', () => {
82
+ expect(
83
+ shouldOpenAdminRouteInNewWindow({
84
+ currentPathname: '/v/test',
85
+ targetPathname: '/admin/settings',
86
+ basePath: '/v',
87
+ adminRoutePath: '/admin',
88
+ replace: true,
89
+ }),
90
+ ).toBe(false);
91
+ expect(
92
+ shouldOpenAdminRouteInNewWindow({
93
+ currentPathname: '/v/test',
94
+ targetPathname: 'admin/settings',
95
+ basePath: '/v',
96
+ adminRoutePath: '/admin',
97
+ }),
98
+ ).toBe(false);
99
+ });
100
+ });
101
+
102
+ it('opens admin route in a new window through the underlying router navigate from a sub-app portal', async () => {
103
+ const app = {
104
+ name: 'a_9xlild35jir',
105
+ getPublicPath: () => '/v/',
106
+ getHref: (pathname: string) => `/v/apps/a_9xlild35jir/${pathname.replace(/^\/+/, '')}`,
107
+ layoutManager: {
108
+ getLayout: () => ({ routePath: '/admin' }),
109
+ },
110
+ renderComponent: () => null,
111
+ } as unknown as BaseApplication<unknown>;
112
+ const manager = new RouterManager({ type: 'browser' }, app);
113
+ const open = vi.spyOn(window, 'open').mockImplementation(() => null);
114
+
115
+ window.history.pushState({}, '', '/v/apps/a_9xlild35jir/crm-amd/ekeisumx1zu');
116
+ manager.getRouterComponent();
117
+ await manager.router.navigate('/admin/settings/version-control/list');
118
+
119
+ expect(open).toHaveBeenCalledWith(
120
+ '/v/apps/a_9xlild35jir/admin/settings/version-control/list',
121
+ '_blank',
122
+ 'noopener,noreferrer',
123
+ );
124
+ });
125
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import {
11
+ define,
12
+ observable,
13
+ type FlowModelContext,
14
+ type SubModelItem,
15
+ type SubModelItemsType,
16
+ } from '@nocobase/flow-engine';
17
+
18
+ export type EntryActionScope = 'action-panel' | 'app-switcher' | string;
19
+
20
+ export type EntryActionProvider = (ctx: FlowModelContext) => SubModelItem[] | Promise<SubModelItem[]>;
21
+
22
+ type ProviderRecord = {
23
+ name: string;
24
+ scope: EntryActionScope;
25
+ provider: EntryActionProvider;
26
+ sort: number;
27
+ };
28
+
29
+ export class EntryActionManager {
30
+ private readonly providers = new Map<string, ProviderRecord>();
31
+ revision = 0;
32
+
33
+ constructor() {
34
+ define(this, {
35
+ revision: observable.ref,
36
+ });
37
+ }
38
+
39
+ register(name: string, options: { scope: EntryActionScope; provider: EntryActionProvider; sort?: number }) {
40
+ this.providers.set(name, {
41
+ name,
42
+ scope: options.scope,
43
+ provider: options.provider,
44
+ sort: options.sort ?? 0,
45
+ });
46
+ this.invalidate();
47
+ }
48
+
49
+ unregister(name: string) {
50
+ this.providers.delete(name);
51
+ this.invalidate();
52
+ }
53
+
54
+ invalidate() {
55
+ this.revision += 1;
56
+ }
57
+
58
+ getItems(scope: EntryActionScope): SubModelItemsType {
59
+ return async (ctx) => {
60
+ const providers = [...this.providers.values()]
61
+ .filter((item) => item.scope === scope)
62
+ .sort((a, b) => a.sort - b.sort);
63
+ const groups = await Promise.all(
64
+ providers.map(async (item) => {
65
+ try {
66
+ return await item.provider(ctx);
67
+ } catch (error) {
68
+ console.error(`[NocoBase] Failed to load entry action provider "${item.name}".`, error);
69
+ return [];
70
+ }
71
+ }),
72
+ );
73
+ return groups.flat();
74
+ };
75
+ }
76
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ export * from './EntryActionManager';
@@ -14,7 +14,7 @@ import {
14
14
  useFlowModelById,
15
15
  useFlowViewContext,
16
16
  } from '@nocobase/flow-engine';
17
- import type { FlowModel, FlowModelRendererProps, ModelConstructor } from '@nocobase/flow-engine';
17
+ import type { FlowEngineContext, FlowModel, FlowModelRendererProps, ModelConstructor } from '@nocobase/flow-engine';
18
18
  import { useRequest } from 'ahooks';
19
19
  import React from 'react';
20
20
  import FlowRoute from './components/FlowRoute';
@@ -57,10 +57,36 @@ type FlowPageProps = {
57
57
  showFlowSettings?: FlowModelRendererProps['showFlowSettings'];
58
58
  };
59
59
 
60
+ type FlowPageViewContext = FlowEngineContext & {
61
+ view?: {
62
+ inputArgs?: {
63
+ isMobileLayout?: unknown;
64
+ };
65
+ };
66
+ };
67
+
68
+ const bindViewLayoutState = (model: FlowModel, ctx?: FlowPageViewContext | null) => {
69
+ const hasViewMobileLayout = typeof ctx?.view?.inputArgs?.isMobileLayout === 'boolean';
70
+ const hasContextMobileLayout = typeof ctx?.isMobileLayout === 'boolean';
71
+ if (!hasViewMobileLayout && !hasContextMobileLayout) {
72
+ return;
73
+ }
74
+
75
+ model.context.defineProperty('isMobileLayout', {
76
+ get: () => {
77
+ if (typeof ctx?.isMobileLayout === 'boolean') {
78
+ return ctx.isMobileLayout;
79
+ }
80
+ return !!ctx?.view?.inputArgs?.isMobileLayout;
81
+ },
82
+ cache: false,
83
+ });
84
+ };
85
+
60
86
  export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknown>) => {
61
87
  const { pageModelClass = 'ChildPageModel', parentId, onModelLoaded, defaultTabTitle, ...rest } = props;
62
88
  const flowEngine = useFlowEngine();
63
- const ctx = useFlowViewContext();
89
+ const ctx = useFlowViewContext<FlowPageViewContext>();
64
90
  const { loading, data, error } = useRequest(
65
91
  async () => {
66
92
  const ModelClass = await flowEngine.getModelClassAsync(pageModelClass);
@@ -105,6 +131,7 @@ export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknow
105
131
  const data = await flowEngine.loadOrCreateModel(options, { skipSave: !flowEngine.context.flowSettingsEnabled });
106
132
  if (data?.uid && onModelLoaded) {
107
133
  data.context.addDelegate(ctx);
134
+ bindViewLayoutState(data, ctx);
108
135
  data.removeParentDelegate();
109
136
  onModelLoaded(data.uid, data);
110
137
  }