@nocobase/client-v2 2.1.31 → 2.2.0-alpha.10

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 (170) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +2 -0
  3. package/es/PluginSettingsManager.d.ts +33 -0
  4. package/es/RouteRepository.d.ts +29 -6
  5. package/es/RouterManager.d.ts +15 -0
  6. package/es/authRedirect.d.ts +1 -0
  7. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  8. package/es/components/category-tabs/index.d.ts +9 -0
  9. package/es/components/form/TypedVariableInput.d.ts +31 -5
  10. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  11. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  12. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  13. package/es/components/form/filter/index.d.ts +2 -0
  14. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  15. package/es/components/index.d.ts +2 -0
  16. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  17. package/es/entry-actions/index.d.ts +9 -0
  18. package/es/flow/actions/index.d.ts +1 -1
  19. package/es/flow/actions/linkageRules.d.ts +2 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  21. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  22. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +3 -0
  23. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  24. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  25. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  26. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  27. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  28. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  29. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  30. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  31. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  32. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  33. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  34. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  35. package/es/flow/index.d.ts +1 -0
  36. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  37. package/es/flow/models/base/GridModel.d.ts +1 -1
  38. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  39. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  40. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  41. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +9 -0
  42. package/es/flow-compat/routeTypes.d.ts +1 -0
  43. package/es/index.d.ts +7 -1
  44. package/es/index.mjs +223 -135
  45. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  46. package/es/utils/index.d.ts +1 -0
  47. package/lib/index.js +220 -132
  48. package/package.json +7 -7
  49. package/src/APIClient.ts +1 -10
  50. package/src/Application.tsx +4 -0
  51. package/src/BaseApplication.tsx +2 -0
  52. package/src/PluginSettingsManager.ts +53 -0
  53. package/src/RouteRepository.ts +151 -24
  54. package/src/RouterManager.tsx +142 -1
  55. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  56. package/src/__tests__/RouteRepository.test.ts +239 -0
  57. package/src/__tests__/RouterManager.test.ts +125 -0
  58. package/src/__tests__/app.test.tsx +18 -0
  59. package/src/__tests__/authRedirect.test.ts +17 -0
  60. package/src/__tests__/browserChecker.test.ts +61 -0
  61. package/src/__tests__/exports.test.ts +16 -0
  62. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  63. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -0
  64. package/src/__tests__/plugin-manager.test.tsx +44 -2
  65. package/src/__tests__/settings-center.test.tsx +40 -1
  66. package/src/authRedirect.ts +38 -0
  67. package/src/components/README.md +7 -1
  68. package/src/components/README.zh-CN.md +6 -1
  69. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  70. package/src/components/category-tabs/index.ts +10 -0
  71. package/src/components/form/JsonTextArea.tsx +4 -0
  72. package/src/components/form/TypedVariableInput.tsx +452 -101
  73. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  74. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  75. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  76. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  77. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  78. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  79. package/src/components/form/filter/index.ts +2 -0
  80. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  81. package/src/components/index.ts +2 -0
  82. package/src/entry-actions/EntryActionManager.ts +76 -0
  83. package/src/entry-actions/index.ts +10 -0
  84. package/src/flow/__tests__/FlowRoute.test.tsx +1084 -145
  85. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  86. package/src/flow/actions/customVariable.tsx +1 -2
  87. package/src/flow/actions/index.ts +2 -0
  88. package/src/flow/actions/linkageRules.tsx +65 -8
  89. package/src/flow/actions/runjs.tsx +2 -14
  90. package/src/flow/admin-shell/BaseLayoutModel.tsx +13 -0
  91. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +46 -13
  92. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +486 -0
  93. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +64 -8
  94. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  95. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  96. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  97. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  98. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  99. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  100. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  101. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +435 -0
  102. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  103. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +77 -0
  104. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +214 -2
  105. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  106. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  107. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  108. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  109. package/src/flow/components/DefaultValue.tsx +1 -2
  110. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  111. package/src/flow/components/FieldAssignValueInput.tsx +76 -9
  112. package/src/flow/components/FlowRoute.tsx +185 -28
  113. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  114. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +84 -2
  115. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  116. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  117. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  118. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  119. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  120. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  121. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  122. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  123. package/src/flow/index.ts +1 -0
  124. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  125. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  126. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  127. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  128. package/src/flow/models/base/ActionModel.tsx +21 -8
  129. package/src/flow/models/base/ActionModelCore.tsx +5 -0
  130. package/src/flow/models/base/GridModel.tsx +38 -7
  131. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  132. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  133. package/src/flow/models/base/PageModel/PageTabModel.tsx +186 -3
  134. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -14
  135. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  136. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  137. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +586 -1
  138. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  139. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  140. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  141. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -2
  142. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  143. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  144. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +87 -0
  145. package/src/flow/models/blocks/form/value-runtime/rules.ts +2 -2
  146. package/src/flow/models/blocks/form/value-runtime/runtime.ts +91 -0
  147. package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
  148. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  149. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
  150. package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
  151. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
  152. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  153. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  154. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  155. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  156. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  157. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  158. package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
  159. package/src/flow-compat/Popover.tsx +43 -4
  160. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  161. package/src/flow-compat/routeTypes.ts +1 -0
  162. package/src/index.ts +13 -1
  163. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  164. package/src/nocobase-buildin-plugin/index.tsx +19 -3
  165. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  166. package/src/settings-center/SystemSettingsPage.tsx +0 -1
  167. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  168. package/src/settings-center/utils.tsx +0 -6
  169. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  170. package/src/utils/index.tsx +1 -0
@@ -180,11 +180,21 @@ const AddFieldColumn = ({ model }: { model: TableBlockModel }) => {
180
180
  ].filter(Boolean)}
181
181
  keepDropdownOpen
182
182
  >
183
- <FlowSettingsButton icon={<SettingOutlined />}>{model.translate('Fields')}</FlowSettingsButton>
183
+ {renderTableSettingsButton(model, 'Fields')}
184
184
  </AddSubModelButton>
185
185
  );
186
186
  };
187
187
 
188
+ function renderTableSettingsButton(model: TableBlockModel, title: string) {
189
+ const label = model.translate(title);
190
+
191
+ return (
192
+ <FlowSettingsButton aria-label={label} icon={<SettingOutlined />}>
193
+ {model.context.isMobileLayout ? null : label}
194
+ </FlowSettingsButton>
195
+ );
196
+ }
197
+
188
198
  type CustomTableBlockModelClassesEnum = {
189
199
  CollectionActionGroupModel?: string;
190
200
  RecordActionGroupModel?: string;
@@ -268,7 +278,7 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
268
278
  cols.push({
269
279
  key: 'addColumn',
270
280
  fixed: 'right',
271
- width: 100,
281
+ ...(this.context.isMobileLayout ? {} : { width: 100 }),
272
282
  title: <AddFieldColumn model={this} />,
273
283
  } as any);
274
284
  }
@@ -520,7 +530,7 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
520
530
  subModelBaseClass={this.getModelClassName('CollectionActionGroupModel')}
521
531
  subModelKey="actions"
522
532
  >
523
- <FlowSettingsButton icon={<SettingOutlined />}>{this.translate('Actions')}</FlowSettingsButton>
533
+ {renderTableSettingsButton(this, 'Actions')}
524
534
  </AddSubModelButton>
525
535
  );
526
536
  }
@@ -19,11 +19,16 @@ const capturedDroppableUids: string[] = [];
19
19
  const capturedRendererProps: any[] = [];
20
20
  let latestOnDragEnd: ((event: any) => void) | undefined;
21
21
  const capturedDndProviders: Array<{ persist: boolean }> = [];
22
+ const capturedAddSubModelButtonProps: any[] = [];
22
23
 
23
24
  vi.mock('@nocobase/flow-engine', async () => {
24
25
  const actual = await vi.importActual<any>('@nocobase/flow-engine');
25
26
  return {
26
27
  ...actual,
28
+ AddSubModelButton: (props) => {
29
+ capturedAddSubModelButtonProps.push(props);
30
+ return <button data-testid="add-sub-model-button">{props.children}</button>;
31
+ },
27
32
  DndProvider: ({ children, onDragEnd, persist = true, ...restProps }) => {
28
33
  const engine = actual.useFlowEngine();
29
34
  const handleDragEnd = (event: any) => {
@@ -50,6 +55,17 @@ vi.mock('@nocobase/flow-engine', async () => {
50
55
  const ActualFlowModelRenderer = actual.FlowModelRenderer;
51
56
  return <ActualFlowModelRenderer {...props} />;
52
57
  },
58
+ FlowsFloatContextMenu: ({ children, extraToolbarItems = [], model }) => {
59
+ return (
60
+ <div data-test-flow-context-menu>
61
+ {children}
62
+ {extraToolbarItems.map((item) => {
63
+ const Component = item.component;
64
+ return <Component key={item.key} model={model} />;
65
+ })}
66
+ </div>
67
+ );
68
+ },
53
69
  };
54
70
  });
55
71
 
@@ -221,6 +237,7 @@ describe('TableActionsColumnModel: drag integration', () => {
221
237
  capturedDroppableUids.length = 0;
222
238
  capturedRendererProps.length = 0;
223
239
  capturedDndProviders.length = 0;
240
+ capturedAddSubModelButtonProps.length = 0;
224
241
  latestOnDragEnd = undefined;
225
242
  });
226
243
 
@@ -233,6 +250,7 @@ describe('TableActionsColumnModel: drag integration', () => {
233
250
  props: { width: 200, title: 'Actions' },
234
251
  subModels: { actions: [{ use: 'TestViewActionModel' }, { use: 'TestViewActionModel' }] },
235
252
  });
253
+ actionsCol.context.defineMethod('getModelClassName', (className: string) => className);
236
254
 
237
255
  const colProps = actionsCol.getColumnProps();
238
256
  const record = { id: 1, phone: '000000' } as any;
@@ -335,4 +353,40 @@ describe('TableActionsColumnModel: drag integration', () => {
335
353
 
336
354
  expect(moveModelSpy).toHaveBeenCalledWith(firstUid, secondUid, { persist: true });
337
355
  });
356
+
357
+ it('disables icon-only mode when adding row actions without icons', async () => {
358
+ const engine = new FlowEngine();
359
+ engine.registerModels({ TableActionsColumnModel, TestViewActionModel });
360
+
361
+ const actionsCol = engine.createModel<TableActionsColumnModel>({
362
+ use: 'TableActionsColumnModel',
363
+ props: { width: 200, title: 'Actions' },
364
+ });
365
+ actionsCol.context.defineMethod('getModelClassName', (className: string) => className);
366
+
367
+ render(
368
+ <FlowEngineProvider engine={engine}>
369
+ <ConfigProvider>
370
+ <App>{actionsCol.getColumnProps().title as React.ReactNode}</App>
371
+ </ConfigProvider>
372
+ </FlowEngineProvider>,
373
+ );
374
+
375
+ const addButtonProps = capturedAddSubModelButtonProps.find(
376
+ (props) => props.subModelKey === 'actions' && props.subModelBaseClass === 'RecordActionGroupModel',
377
+ );
378
+ expect(addButtonProps).toBeDefined();
379
+ expect(addButtonProps.afterSubModelInit).toBeTypeOf('function');
380
+
381
+ const actionModel = {
382
+ setStepParams: vi.fn(),
383
+ };
384
+ await addButtonProps.afterSubModelInit(actionModel);
385
+
386
+ expect(actionModel.setStepParams).toHaveBeenCalledWith('buttonSettings', 'general', {
387
+ type: 'link',
388
+ icon: null,
389
+ iconOnly: false,
390
+ });
391
+ });
338
392
  });
@@ -0,0 +1,78 @@
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 { FlowEngine } from '@nocobase/flow-engine';
11
+ import '@nocobase/client';
12
+ import React from 'react';
13
+ import { describe, expect, it } from 'vitest';
14
+ import { TableBlockModel } from '../TableBlockModel';
15
+
16
+ function createTableModel(options: { isMobileLayout?: boolean; flowSettingsEnabled?: boolean } = {}) {
17
+ const engine = new FlowEngine();
18
+ engine.registerModels({ TableBlockModel });
19
+ engine.context.defineProperty('isMobileLayout', { value: !!options.isMobileLayout });
20
+ engine.context.defineProperty('flowSettingsEnabled', { value: !!options.flowSettingsEnabled });
21
+
22
+ const ds = engine.dataSourceManager.getDataSource('main');
23
+ ds.addCollection({
24
+ name: 'posts',
25
+ filterTargetKey: 'id',
26
+ fields: [
27
+ { name: 'id', type: 'integer', interface: 'number' },
28
+ { name: 'title', type: 'string', interface: 'input' },
29
+ ],
30
+ });
31
+
32
+ return engine.createModel<TableBlockModel>({
33
+ uid: `posts-table-${options.isMobileLayout ? 'mobile' : 'desktop'}`,
34
+ use: 'TableBlockModel',
35
+ stepParams: {
36
+ resourceSettings: {
37
+ init: {
38
+ dataSourceKey: 'main',
39
+ collectionName: 'posts',
40
+ },
41
+ },
42
+ },
43
+ });
44
+ }
45
+
46
+ function getSettingsButton(element: React.ReactElement) {
47
+ return element.props.children as React.ReactElement;
48
+ }
49
+
50
+ describe('TableBlockModel mobile settings buttons', () => {
51
+ it('renders table actions and fields settings as icon-only buttons in mobile layouts', () => {
52
+ const model = createTableModel({ isMobileLayout: true, flowSettingsEnabled: true });
53
+ const actionButton = getSettingsButton(model.renderConfigureActions() as React.ReactElement);
54
+ const addColumn = model.getColumns().find((column) => column.key === 'addColumn');
55
+ const fieldsButton = getSettingsButton(
56
+ (addColumn?.title as React.ReactElement).type({ model }) as React.ReactElement,
57
+ );
58
+
59
+ expect(actionButton.props.children).toBeNull();
60
+ expect(actionButton.props['aria-label']).toBe('Actions');
61
+ expect(fieldsButton.props.children).toBeNull();
62
+ expect(fieldsButton.props['aria-label']).toBe('Fields');
63
+ expect(addColumn?.width).toBeUndefined();
64
+ });
65
+
66
+ it('keeps table actions and fields settings labels outside mobile layouts', () => {
67
+ const model = createTableModel({ flowSettingsEnabled: true });
68
+ const actionButton = getSettingsButton(model.renderConfigureActions() as React.ReactElement);
69
+ const addColumn = model.getColumns().find((column) => column.key === 'addColumn');
70
+ const fieldsButton = getSettingsButton(
71
+ (addColumn?.title as React.ReactElement).type({ model }) as React.ReactElement,
72
+ );
73
+
74
+ expect(actionButton.props.children).toBe('Actions');
75
+ expect(fieldsButton.props.children).toBe('Fields');
76
+ expect(addColumn?.width).toBe(100);
77
+ });
78
+ });
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { ElementProxy, tExpr, createSafeDocument, createSafeWindow, createSafeNavigator } from '@nocobase/flow-engine';
10
+ import { ElementProxy, tExpr } from '@nocobase/flow-engine';
11
11
  import React, { useEffect, useMemo, useRef } from 'react';
12
12
  import { Input } from 'antd';
13
13
  import { FieldModel } from '../base/FieldModel';
@@ -398,16 +398,7 @@ JSEditableFieldModel.registerFlow({
398
398
  get: () => isReadOnlyMode(ctx.model),
399
399
  cache: false,
400
400
  });
401
- const navigator = createSafeNavigator();
402
- await ctx.runjs(
403
- code,
404
- {
405
- window: createSafeWindow({ navigator }),
406
- document: createSafeDocument(),
407
- navigator,
408
- },
409
- { version },
410
- );
401
+ await ctx.runjs(code, undefined, { version });
411
402
  });
412
403
  },
413
404
  },
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { ElementProxy, tExpr, createSafeWindow, createSafeDocument, createSafeNavigator } from '@nocobase/flow-engine';
10
+ import { ElementProxy, tExpr } from '@nocobase/flow-engine';
11
11
  import React, { useEffect } from 'react';
12
12
  import { FieldModel } from '../base/FieldModel';
13
13
  import { resolveRunJsParams } from '../utils/resolveRunJsParams';
@@ -211,12 +211,7 @@ JSFieldModel.registerFlow({
211
211
  get: () => ctx.model.props?.value,
212
212
  cache: false,
213
213
  });
214
- const navigator = createSafeNavigator();
215
- await ctx.runjs(
216
- code,
217
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
218
- { version },
219
- );
214
+ await ctx.runjs(code, undefined, { version });
220
215
  });
221
216
  },
222
217
  },
@@ -7,14 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import {
11
- ElementProxy,
12
- FormItem,
13
- createSafeDocument,
14
- createSafeWindow,
15
- createSafeNavigator,
16
- tExpr,
17
- } from '@nocobase/flow-engine';
10
+ import { ElementProxy, FormItem, tExpr } from '@nocobase/flow-engine';
18
11
  import React from 'react';
19
12
  import { CodeEditor } from '../../components/code-editor';
20
13
  import { CommonItemModel } from '../base/CommonItemModel';
@@ -150,12 +143,7 @@ ctx.render(<JsItem />);
150
143
  ctx.defineProperty('element', {
151
144
  get: () => new ElementProxy(element),
152
145
  });
153
- const navigator = createSafeNavigator();
154
- await ctx.runjs(
155
- code,
156
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
157
- { version },
158
- );
146
+ await ctx.runjs(code, undefined, { version });
159
147
  });
160
148
  },
161
149
  },
@@ -48,12 +48,12 @@ export class VariableFieldFormModel extends FlowModel {
48
48
  <FormProvider form={this.form}>
49
49
  <FormLayout layout={'vertical'}>
50
50
  {this.mapSubModels('fields', (field) => {
51
- // 确保字段模型具备稳定的 id/name,便于依赖路径的组件(如公式字段)正确解析
51
+ // Avoid conflicting with the DOM nodeName property when rendering the temporary input.
52
52
  const init = field?.getStepParams?.('fieldSettings', 'init') || {};
53
53
  const fp = init?.fieldPath as string | undefined;
54
54
  if (fp) {
55
- const namePath = fp.includes('.') ? fp.split('.') : [fp];
56
- const toSet: any = {};
55
+ const namePath = (fp === 'nodeName' ? '__nb_variable_field_nodeName' : fp).split('.');
56
+ const toSet: Record<string, unknown> = {};
57
57
  if (!field?.props?.id) toSet.id = namePath;
58
58
  if (!field?.props?.name) toSet.name = namePath;
59
59
  if (Object.keys(toSet).length) field?.setProps?.(toSet);
@@ -0,0 +1,51 @@
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 { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
11
+ import { render, screen, waitFor } from '@testing-library/react';
12
+ import React from 'react';
13
+ import { describe, expect, it } from 'vitest';
14
+ import { InputFieldModel } from '../InputFieldModel';
15
+ import { VariableFieldFormModel } from '../VariableFieldFormModel';
16
+
17
+ describe('VariableFieldFormModel', () => {
18
+ it('uses DOM-safe name and id for temporary controls created from reserved form property names', async () => {
19
+ const engine = new FlowEngine();
20
+ engine.registerModels({ InputFieldModel, VariableFieldFormModel });
21
+ const model = engine.createModel<VariableFieldFormModel>({
22
+ use: 'VariableFieldFormModel',
23
+ subModels: {
24
+ fields: [
25
+ {
26
+ use: 'InputFieldModel',
27
+ stepParams: {
28
+ fieldSettings: {
29
+ init: {
30
+ fieldPath: 'nodeName',
31
+ },
32
+ },
33
+ },
34
+ },
35
+ ],
36
+ },
37
+ });
38
+ const field = model.subModels.fields[0];
39
+
40
+ render(<FlowEngineProvider engine={engine}>{model.render()}</FlowEngineProvider>);
41
+
42
+ const input = await screen.findByRole('textbox');
43
+ await waitFor(() => {
44
+ expect(field.props.name).toEqual(['__nb_variable_field_nodeName']);
45
+ });
46
+ expect(input).toHaveAttribute('name', '__nb_variable_field_nodeName');
47
+ expect(input).toHaveAttribute('id', '__nb_variable_field_nodeName');
48
+ expect(field.props.id).toEqual(['__nb_variable_field_nodeName']);
49
+ expect(field.getStepParams('fieldSettings', 'init')).toEqual({ fieldPath: 'nodeName' });
50
+ });
51
+ });
@@ -14,9 +14,11 @@ import { Button, Dropdown, theme, Tooltip, type ButtonProps, type MenuProps } fr
14
14
  import React, { useEffect, useMemo, useState } from 'react';
15
15
  import { useHotkeys } from 'react-hotkeys-hook';
16
16
  import { useTranslation } from 'react-i18next';
17
- import { Link } from 'react-router-dom';
17
+ import { Link, useLocation } from 'react-router-dom';
18
18
  import { useACLRoleContext } from '../../../acl';
19
+ import type { BaseApplication } from '../../../BaseApplication';
19
20
  import type { PluginSettingsPageType } from '../../../PluginSettingsManager';
21
+ import { shouldOpenAdminRouteInNewWindow } from '../../../RouterManager';
20
22
  import { useApp } from '../../../hooks/useApp';
21
23
  import {
22
24
  filterRenderableSettings,
@@ -64,6 +66,163 @@ const topbarActionTriggerClassName = css`
64
66
  height: 100%;
65
67
  `;
66
68
 
69
+ type TopbarSettingsAppLike = Pick<
70
+ BaseApplication<any>,
71
+ 'name' | 'router' | 'getPublicPath' | 'getHref' | 'layoutManager'
72
+ >;
73
+
74
+ const normalizeTopbarPath = (pathname?: string) => {
75
+ const trimmed = pathname?.trim();
76
+ if (!trimmed || trimmed === '/') {
77
+ return '/';
78
+ }
79
+ return `/${trimmed.replace(/^\/+/, '')}`;
80
+ };
81
+
82
+ const normalizeTopbarBasePath = (pathname?: string) => {
83
+ const normalized = normalizeTopbarPath(pathname).replace(/\/+$/, '');
84
+ return normalized === '' || normalized === '/' ? '' : normalized;
85
+ };
86
+
87
+ const getTopbarRouterBasePath = (app: TopbarSettingsAppLike | undefined) => {
88
+ return app?.router?.getBasename?.() || app?.router?.basename || app?.getPublicPath?.() || '';
89
+ };
90
+
91
+ const stripTopbarRouterBasePath = (pathname: string, basename?: string) => {
92
+ const normalizedPath = normalizeTopbarPath(pathname);
93
+ const normalizedBase = normalizeTopbarBasePath(basename);
94
+
95
+ if (!normalizedBase) {
96
+ return normalizedPath;
97
+ }
98
+
99
+ if (normalizedPath === normalizedBase) {
100
+ return '/';
101
+ }
102
+
103
+ if (normalizedPath.startsWith(`${normalizedBase}/`)) {
104
+ return normalizeTopbarPath(normalizedPath.slice(normalizedBase.length));
105
+ }
106
+
107
+ return normalizedPath;
108
+ };
109
+
110
+ const getTopbarAppPath = (pathname: string) => {
111
+ const normalizedPath = normalizeTopbarPath(pathname);
112
+ const match = /^(.*?\/(?:apps|_app)\/[^/]+)(?=\/|$)/.exec(normalizedPath);
113
+ const appPath = match?.[1] || '';
114
+
115
+ return {
116
+ appPath,
117
+ routePath: appPath ? normalizeTopbarPath(normalizedPath.slice(appPath.length)) : normalizedPath,
118
+ };
119
+ };
120
+
121
+ const getTopbarContextAppPath = (app: TopbarSettingsAppLike | undefined, basename?: string) => {
122
+ const basenameAppPath = getTopbarAppPath(basename || '').appPath;
123
+ if (basenameAppPath) {
124
+ return basenameAppPath;
125
+ }
126
+
127
+ const publicPathAppPath = getTopbarAppPath(app?.getPublicPath?.() || '').appPath;
128
+ if (publicPathAppPath) {
129
+ return publicPathAppPath;
130
+ }
131
+
132
+ if (app?.name && app.name !== 'main' && app.getHref) {
133
+ return normalizeTopbarBasePath(app.getHref('/'));
134
+ }
135
+
136
+ return '';
137
+ };
138
+
139
+ const prependTopbarAppPath = (pathname: string, appPath: string) => {
140
+ const normalizedPath = normalizeTopbarPath(pathname);
141
+
142
+ if (!appPath || normalizedPath === appPath || normalizedPath.startsWith(`${appPath}/`)) {
143
+ return normalizedPath;
144
+ }
145
+
146
+ return `${appPath}${normalizedPath}`;
147
+ };
148
+
149
+ const isAdminRuntimePath = (pathname: string) => {
150
+ return pathname === '/admin' || pathname.startsWith('/admin/');
151
+ };
152
+
153
+ const getTopbarAdminRoutePath = (app: TopbarSettingsAppLike | undefined) => {
154
+ try {
155
+ return app?.layoutManager?.getLayout?.('admin')?.routePath;
156
+ } catch {
157
+ return undefined;
158
+ }
159
+ };
160
+
161
+ const buildTopbarDocumentHref = (targetPath: string, basename?: string) => {
162
+ const normalizedTarget = normalizeTopbarPath(targetPath);
163
+ const normalizedBase = normalizeTopbarBasePath(basename);
164
+
165
+ if (!normalizedBase || normalizedTarget === normalizedBase || normalizedTarget.startsWith(`${normalizedBase}/`)) {
166
+ return normalizedTarget;
167
+ }
168
+
169
+ return `${normalizedBase}${normalizedTarget}`;
170
+ };
171
+
172
+ function TopbarExternalSettingsLabel(props: { title: React.ReactNode; link: string }) {
173
+ return (
174
+ <div
175
+ onClick={() => {
176
+ window.open(props.link, '_blank', 'noopener,noreferrer');
177
+ }}
178
+ >
179
+ {props.title}
180
+ </div>
181
+ );
182
+ }
183
+
184
+ function TopbarInternalSettingsLabel(props: { title: React.ReactNode; path?: string }) {
185
+ const app = useApp();
186
+ const location = useLocation();
187
+ const targetPath = props.path || '/admin/settings';
188
+ const basename = getTopbarRouterBasePath(app);
189
+ const currentPath = stripTopbarRouterBasePath(location.pathname, basename);
190
+ const currentLocationAppPath = getTopbarAppPath(currentPath);
191
+ const currentAppPath = currentLocationAppPath.appPath || getTopbarContextAppPath(app, basename);
192
+ const currentRoutePath = currentLocationAppPath.appPath ? currentLocationAppPath.routePath : currentPath;
193
+ const targetPathInCurrentApp = prependTopbarAppPath(stripTopbarRouterBasePath(targetPath, basename), currentAppPath);
194
+
195
+ if (currentAppPath) {
196
+ const href = buildTopbarDocumentHref(targetPathInCurrentApp, basename);
197
+ const shouldOpenInNewWindow = shouldOpenAdminRouteInNewWindow({
198
+ currentPathname: currentPath,
199
+ targetPathname: targetPathInCurrentApp,
200
+ basePath: basename,
201
+ adminRoutePath: getTopbarAdminRoutePath(app),
202
+ });
203
+
204
+ return (
205
+ <a
206
+ href={href}
207
+ target={shouldOpenInNewWindow ? '_blank' : undefined}
208
+ rel={shouldOpenInNewWindow ? 'noopener noreferrer' : undefined}
209
+ >
210
+ {props.title}
211
+ </a>
212
+ );
213
+ }
214
+
215
+ if (isAdminRuntimePath(currentRoutePath)) {
216
+ return <Link to={targetPathInCurrentApp}>{props.title}</Link>;
217
+ }
218
+
219
+ return (
220
+ <a href={buildTopbarDocumentHref(targetPath, basename)} target="_blank" rel="noopener noreferrer">
221
+ {props.title}
222
+ </a>
223
+ );
224
+ }
225
+
67
226
  /**
68
227
  * 将 settings 顶部菜单转换成右上角 dropdown items。
69
228
  *
@@ -112,15 +271,9 @@ export function getTopbarPluginSettingsItems(options: {
112
271
  title: targetTitle,
113
272
  icon: item.icon,
114
273
  label: targetLink ? (
115
- <div
116
- onClick={() => {
117
- window.open(targetLink, '_blank', 'noopener,noreferrer');
118
- }}
119
- >
120
- {targetTitle}
121
- </div>
274
+ <TopbarExternalSettingsLabel title={targetTitle} link={targetLink} />
122
275
  ) : (
123
- <Link to={targetPath || '/admin/settings'}>{targetTitle}</Link>
276
+ <TopbarInternalSettingsLabel title={targetTitle} path={targetPath} />
124
277
  ),
125
278
  };
126
279
  });
@@ -131,7 +284,12 @@ export function getTopbarPluginSettingsItems(options: {
131
284
  items.push({
132
285
  key: pluginManagerSetting.key,
133
286
  icon: pluginManagerSetting.icon || <ApiOutlined />,
134
- label: <Link to={pluginManagerSetting.path}>{pluginManagerSetting.title || t('Plugin manager')}</Link>,
287
+ label: (
288
+ <TopbarInternalSettingsLabel
289
+ title={pluginManagerSetting.title || t('Plugin manager')}
290
+ path={pluginManagerSetting.path}
291
+ />
292
+ ),
135
293
  });
136
294
  }
137
295
 
@@ -8,15 +8,54 @@
8
8
  */
9
9
 
10
10
  import { Popover as AntdPopover, type PopoverProps } from 'antd';
11
- import React, { useCallback, useRef } from 'react';
11
+ import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
12
12
  import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext';
13
13
 
14
14
  export const ICON_POPUP_Z_INDEX = 2000;
15
15
 
16
+ function parseZIndex(value?: string | null) {
17
+ if (!value || value === 'auto') {
18
+ return;
19
+ }
20
+
21
+ const zIndex = Number(value);
22
+ return Number.isFinite(zIndex) ? zIndex : undefined;
23
+ }
24
+
25
+ function getElementZIndex(element: HTMLElement) {
26
+ return parseZIndex(window.getComputedStyle(element).zIndex) ?? parseZIndex(element.style.zIndex);
27
+ }
28
+
29
+ function getContainingPopupZIndex(element: HTMLElement | null) {
30
+ if (!element || typeof window === 'undefined') {
31
+ return 0;
32
+ }
33
+
34
+ let current: HTMLElement | null = element;
35
+ let maxZIndex = 0;
36
+
37
+ while (current && current !== document.documentElement) {
38
+ const zIndex = getElementZIndex(current);
39
+ if (typeof zIndex === 'number' && zIndex > maxZIndex) {
40
+ maxZIndex = zIndex;
41
+ }
42
+ current = current.parentElement;
43
+ }
44
+
45
+ return maxZIndex;
46
+ }
47
+
16
48
  export const StablePopover = (props: PopoverProps) => {
17
49
  const parentZIndex = useZIndexContext();
18
- const zIndex = getZIndex('drawer', parentZIndex, 1);
19
- const target = useRef(null);
50
+ const [containingPopupZIndex, setContainingPopupZIndex] = useState(parentZIndex);
51
+ const target = useRef<EventTarget | null>(null);
52
+ const triggerRef = useRef<HTMLDivElement | null>(null);
53
+ const zIndex = getZIndex('drawer', Math.max(parentZIndex, containingPopupZIndex), 1);
54
+
55
+ useLayoutEffect(() => {
56
+ const nextZIndex = Math.max(parentZIndex, getContainingPopupZIndex(triggerRef.current));
57
+ setContainingPopupZIndex((previousZIndex) => (previousZIndex === nextZIndex ? previousZIndex : nextZIndex));
58
+ }, [parentZIndex]);
20
59
 
21
60
  const avoidClose = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
22
61
  target.current = e.target;
@@ -36,7 +75,7 @@ export const StablePopover = (props: PopoverProps) => {
36
75
  );
37
76
 
38
77
  return (
39
- <div className="popover-with-stop-propagation" onClick={avoidClose}>
78
+ <div ref={triggerRef} className="popover-with-stop-propagation" onClick={avoidClose}>
40
79
  <zIndexContext.Provider value={zIndex}>
41
80
  <AntdPopover {...props} zIndex={zIndex} onOpenChange={onOpenChange} />
42
81
  </zIndexContext.Provider>
@@ -0,0 +1,34 @@
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 { render, screen, waitFor } from '@testing-library/react';
11
+ import React from 'react';
12
+ import { describe, expect, it } from 'vitest';
13
+ import { StablePopover } from '../Popover';
14
+
15
+ describe('StablePopover', () => {
16
+ it('renders above the containing popup layer', async () => {
17
+ render(
18
+ <div style={{ position: 'relative', zIndex: 5000 }}>
19
+ <StablePopover open content={<div>Icon picker popup</div>}>
20
+ <button type="button">Select icon</button>
21
+ </StablePopover>
22
+ </div>,
23
+ );
24
+
25
+ expect(screen.getByText('Icon picker popup')).toBeInTheDocument();
26
+
27
+ await waitFor(() => {
28
+ const popover = document.querySelector<HTMLElement>('.ant-popover');
29
+
30
+ expect(popover).toBeInTheDocument();
31
+ expect(Number(popover?.style.zIndex)).toBeGreaterThan(5000);
32
+ });
33
+ });
34
+ });
@@ -18,6 +18,7 @@ export enum NocoBaseDesktopRouteType {
18
18
 
19
19
  export interface NocoBaseDesktopRouteOptions {
20
20
  hasPersistedMenuInstanceFlow?: boolean;
21
+ hasPersistedPageTabFlowModel?: boolean;
21
22
  [key: string]: any;
22
23
  }
23
24