@nocobase/client-v2 2.1.0-beta.33 → 2.1.0-beta.35

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 (76) hide show
  1. package/es/APIClient.d.ts +16 -0
  2. package/es/Application.d.ts +2 -1
  3. package/es/BaseApplication.d.ts +6 -0
  4. package/es/PluginManager.d.ts +2 -0
  5. package/es/authRedirect.d.ts +9 -16
  6. package/es/components/form/EnvVariableInput.d.ts +8 -6
  7. package/es/components/form/VariableInput.d.ts +73 -0
  8. package/es/components/form/index.d.ts +1 -0
  9. package/es/components/form/table/RowOverlayPreview.d.ts +27 -0
  10. package/es/components/form/table/SelectionCell.d.ts +36 -0
  11. package/es/components/form/table/Table.d.ts +82 -0
  12. package/es/components/form/table/constants.d.ts +15 -0
  13. package/es/components/form/table/dnd/SortableRow.d.ts +40 -0
  14. package/es/components/form/table/dnd/index.d.ts +9 -0
  15. package/es/components/form/table/index.d.ts +9 -0
  16. package/es/components/form/table/styles.d.ts +41 -0
  17. package/es/components/form/table/utils.d.ts +44 -0
  18. package/es/components/index.d.ts +2 -0
  19. package/es/flow/components/TextAreaWithContextSelector.d.ts +15 -0
  20. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
  21. package/es/flow/models/blocks/table/dragSort/dragSortComponents.d.ts +1 -6
  22. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +5 -1
  23. package/es/flow-compat/passwordUtils.d.ts +1 -1
  24. package/es/index.d.ts +1 -0
  25. package/es/index.mjs +166 -99
  26. package/es/json-logic/globalOperators.d.ts +11 -0
  27. package/es/theme/globalStyles.d.ts +9 -0
  28. package/es/theme/index.d.ts +1 -0
  29. package/es/utils/globalDeps.d.ts +7 -0
  30. package/lib/index.js +173 -106
  31. package/package.json +9 -6
  32. package/src/APIClient.ts +68 -0
  33. package/src/Application.tsx +6 -2
  34. package/src/BaseApplication.tsx +8 -0
  35. package/src/PluginManager.ts +2 -0
  36. package/src/__tests__/app.test.tsx +8 -0
  37. package/src/__tests__/authRedirect.test.ts +170 -64
  38. package/src/__tests__/globalDeps.test.ts +2 -0
  39. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +6 -6
  40. package/src/__tests__/remotePlugins.test.ts +148 -0
  41. package/src/authRedirect.ts +23 -84
  42. package/src/components/form/EnvVariableInput.tsx +11 -46
  43. package/src/components/form/VariableInput.tsx +177 -0
  44. package/src/components/form/__tests__/EnvVariableInput.test.tsx +175 -0
  45. package/src/components/form/index.tsx +1 -0
  46. package/src/components/form/table/RowOverlayPreview.tsx +51 -0
  47. package/src/components/form/table/SelectionCell.tsx +72 -0
  48. package/src/components/form/table/Table.tsx +279 -0
  49. package/src/components/form/table/__tests__/Table.pagination.test.tsx +80 -0
  50. package/src/components/form/table/constants.ts +16 -0
  51. package/src/components/form/table/dnd/SortableRow.tsx +106 -0
  52. package/src/components/form/table/dnd/index.ts +10 -0
  53. package/src/components/form/table/index.tsx +13 -0
  54. package/src/components/form/table/styles.ts +110 -0
  55. package/src/components/form/table/utils.ts +75 -0
  56. package/src/components/index.ts +2 -0
  57. package/src/css-variable/CSSVariableProvider.tsx +1 -1
  58. package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
  59. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +2 -0
  60. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +111 -0
  61. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -1
  62. package/src/flow/components/TextAreaWithContextSelector.tsx +30 -6
  63. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +81 -0
  64. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +34 -2
  65. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
  66. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
  67. package/src/flow/models/blocks/table/dragSort/dragSortComponents.tsx +1 -81
  68. package/src/flow/models/fields/JSEditableFieldModel.tsx +107 -7
  69. package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +97 -0
  70. package/src/index.ts +1 -0
  71. package/src/json-logic/globalOperators.js +731 -0
  72. package/src/nocobase-buildin-plugin/index.tsx +4 -4
  73. package/src/theme/globalStyles.ts +21 -0
  74. package/src/theme/index.tsx +1 -0
  75. package/src/utils/globalDeps.ts +50 -30
  76. package/src/utils/remotePlugins.ts +107 -6
@@ -0,0 +1,106 @@
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 { MenuOutlined } from '@ant-design/icons';
11
+ import { TinyColor } from '@ctrl/tinycolor';
12
+ import { useSortable } from '@dnd-kit/sortable';
13
+ import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
14
+ import type { DraggableAttributes } from '@dnd-kit/core';
15
+ import { css } from '@emotion/css';
16
+ import { theme } from 'antd';
17
+ import classNames from 'classnames';
18
+ import React, { useMemo } from 'react';
19
+
20
+ type DragSortRowContextValue = {
21
+ attributes?: DraggableAttributes;
22
+ listeners?: SyntheticListenerMap;
23
+ setActivatorNodeRef?: (node: HTMLElement | null) => void;
24
+ };
25
+
26
+ export const DragSortRowContext = React.createContext<DragSortRowContextValue | null>(null);
27
+
28
+ const sortHandleClass = css`
29
+ display: inline-flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ cursor: grab;
33
+ `;
34
+
35
+ /**
36
+ * Activator handle that initiates a row drag. Reads `attributes` / `listeners`
37
+ * / `setActivatorNodeRef` from the surrounding `DragSortRowContext` provided
38
+ * by `SortableRow`, so the handle can sit anywhere within the row's cells
39
+ * (typically a dedicated first column).
40
+ */
41
+ export const SortHandle: React.FC<{ id?: string | number; style?: React.CSSProperties }> = (props) => {
42
+ const { id: _id, ...otherProps } = props;
43
+ const dragSortContext = React.useContext(DragSortRowContext);
44
+ return (
45
+ <span
46
+ ref={dragSortContext?.setActivatorNodeRef}
47
+ {...dragSortContext?.attributes}
48
+ {...dragSortContext?.listeners}
49
+ {...otherProps}
50
+ className={classNames(sortHandleClass)}
51
+ >
52
+ <MenuOutlined />
53
+ </span>
54
+ );
55
+ };
56
+
57
+ /**
58
+ * Drop-in replacement for antd Table's `<tr>` body row that wires `useSortable`
59
+ * keyed by the `data-row-key` attribute antd injects. Pass via
60
+ * `Table.components.body.row` and wrap the `<tbody>` with `DndContext` +
61
+ * `SortableContext`. The `DragSortRowContext` it provides lets `SortHandle`
62
+ * be placed anywhere inside the row, not just on the row itself.
63
+ */
64
+ export const SortableRow: React.FC<{
65
+ rowIndex?: number;
66
+ className?: string;
67
+ [key: string]: any;
68
+ }> = (props) => {
69
+ const { token }: any = theme.useToken();
70
+ const id = props['data-row-key']?.toString();
71
+ const { setNodeRef, setActivatorNodeRef, attributes, listeners, active, over } = useSortable({
72
+ id,
73
+ });
74
+ const { rowIndex, ...others } = props;
75
+ const isOver = over?.id === id;
76
+ const classObj = useMemo(() => {
77
+ const borderColor = new TinyColor(token.colorPrimary).setAlpha(0.6).toHex8String();
78
+ return {
79
+ topActiveClass: css`
80
+ & > td {
81
+ border-top: 2px solid ${borderColor} !important;
82
+ }
83
+ `,
84
+ bottomActiveClass: css`
85
+ & > td {
86
+ border-bottom: 2px solid ${borderColor} !important;
87
+ }
88
+ `,
89
+ };
90
+ }, [token.colorPrimary]);
91
+
92
+ const className =
93
+ (active?.data.current?.sortable.index ?? -1) > rowIndex ? classObj.topActiveClass : classObj.bottomActiveClass;
94
+
95
+ return (
96
+ <DragSortRowContext.Provider value={{ listeners, attributes, setActivatorNodeRef }}>
97
+ <tr
98
+ ref={(node) => {
99
+ setNodeRef(node);
100
+ }}
101
+ {...others}
102
+ className={classNames(props.className, { [className]: active && isOver })}
103
+ />
104
+ </DragSortRowContext.Provider>
105
+ );
106
+ };
@@ -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 './SortableRow';
@@ -0,0 +1,13 @@
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
+ // Public surface of the v2 table primitive. Internal helpers (utils, styles,
11
+ // SelectionCell, RowOverlayPreview, etc.) stay unexported — they're
12
+ // implementation details of `Table` and are not part of the package API.
13
+ export * from './Table';
@@ -0,0 +1,110 @@
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 { css } from '@emotion/css';
11
+ import { SORT_HANDLE_GUTTER } from './constants';
12
+
13
+ /**
14
+ * Reserve a `SORT_HANDLE_GUTTER`-wide gap on the left of the rowSelection
15
+ * column so the handle's `left:0` lands inside a `position:relative` cell.
16
+ * Padding is mirrored on both the header `<th>` and body `<td>` so the
17
+ * "select all" checkbox stays vertically aligned with body checkboxes.
18
+ *
19
+ * The class is a module-level constant — emotion's hash is stable across
20
+ * re-renders, so the caller doesn't need a `useMemo` to keep referential
21
+ * equality.
22
+ */
23
+ export const selectionGutterClassName = css`
24
+ .ant-table-thead > tr > th.ant-table-selection-column,
25
+ .ant-table-tbody > tr > td.ant-table-selection-column {
26
+ padding-left: ${SORT_HANDLE_GUTTER}px !important;
27
+ position: relative;
28
+ }
29
+ `;
30
+
31
+ /**
32
+ * Index ↔ checkbox hover swap CSS. Both `.nb-table-index` and the antd
33
+ * checkbox (wrapped in `.nb-origin-node`) are absolutely positioned and
34
+ * centered inside `.nb-row-selection-cell`, so they share the same anchor
35
+ * without one displacing the other. `display: none/flex` flips so they never
36
+ * overlap mid-transition.
37
+ */
38
+ export const indexSwapClassName = css`
39
+ .ant-table-tbody > tr > td.ant-table-selection-column {
40
+ .nb-row-selection-cell {
41
+ position: relative;
42
+ display: inline-block;
43
+ min-width: 22px;
44
+ min-height: 22px;
45
+ vertical-align: middle;
46
+ }
47
+ .nb-row-selection-cell .nb-table-index,
48
+ .nb-row-selection-cell .nb-origin-node {
49
+ position: absolute;
50
+ inset: 0;
51
+ display: flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ }
55
+ .nb-row-selection-cell .nb-origin-node {
56
+ display: none;
57
+ }
58
+ .nb-row-selection-cell.checked .nb-table-index {
59
+ display: none;
60
+ }
61
+ .nb-row-selection-cell.checked .nb-origin-node {
62
+ display: flex;
63
+ }
64
+ }
65
+ .ant-table-tbody > tr:hover > td.ant-table-selection-column {
66
+ .nb-row-selection-cell .nb-table-index {
67
+ display: none;
68
+ }
69
+ .nb-row-selection-cell .nb-origin-node {
70
+ display: flex;
71
+ }
72
+ }
73
+ `;
74
+
75
+ /**
76
+ * Self-contained styles for the drag-overlay clone. The runtime
77
+ * `selectionGutterClassName` + `indexSwapClassName` are scoped to AntdTable's
78
+ * emotion hash, so they don't reach the cloned `<tr>` injected via
79
+ * `dangerouslySetInnerHTML`. This class replays the minimal subset that the
80
+ * clone needs:
81
+ * - selection column gutter + `position: relative` so the absolute handle
82
+ * lands correctly
83
+ * - hide the row index inside the overlay (drag preview shouldn't carry
84
+ * a stale ordinal)
85
+ * - force the checkbox (`.nb-origin-node`) absolute-centered and visible
86
+ * regardless of hover/checked state, so the selection cell isn't empty
87
+ */
88
+ export const overlayCellStylesClassName = css`
89
+ .ant-table-cell.ant-table-selection-column {
90
+ padding-left: ${SORT_HANDLE_GUTTER}px !important;
91
+ position: relative;
92
+ }
93
+ .nb-table-index {
94
+ display: none !important;
95
+ }
96
+ .nb-row-selection-cell {
97
+ position: relative;
98
+ display: inline-block;
99
+ min-width: 22px;
100
+ min-height: 22px;
101
+ vertical-align: middle;
102
+ }
103
+ .nb-row-selection-cell .nb-origin-node {
104
+ position: absolute;
105
+ inset: 0;
106
+ display: flex !important;
107
+ align-items: center;
108
+ justify-content: center;
109
+ }
110
+ `;
@@ -0,0 +1,75 @@
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 React from 'react';
11
+
12
+ /**
13
+ * Same shape as antd Table's `rowKey` prop — either a record key name or a
14
+ * function. Hoisted here so utilities and `Table.tsx` agree on the contract.
15
+ */
16
+ export type RowKey<RecordType extends object> =
17
+ | (keyof RecordType & (string | number))
18
+ | ((record: RecordType, index?: number) => React.Key);
19
+
20
+ /**
21
+ * Read a stable row id off a record. Mirrors antd Table's rowKey resolution
22
+ * but normalises non-primitive ids to strings so `data-row-key` attributes
23
+ * and `useSortable({ id })` agree on equality.
24
+ */
25
+ export function readRowKey<RecordType extends object>(
26
+ record: RecordType,
27
+ rowKey: RowKey<RecordType>,
28
+ index?: number,
29
+ ): React.Key | undefined {
30
+ if (typeof rowKey === 'function') {
31
+ return rowKey(record, index);
32
+ }
33
+ const value = record[rowKey] as unknown;
34
+ if (value == null) return undefined;
35
+ return typeof value === 'string' || typeof value === 'number' ? value : String(value);
36
+ }
37
+
38
+ /**
39
+ * Pixel-perfect snapshot of a rendered `<tr>` for the drag overlay clone.
40
+ * Contains everything needed to rebuild a visually identical floating row
41
+ * without re-running antd's column layout pass.
42
+ */
43
+ export type RowSnapshot = {
44
+ /** outerHTML of the source `<tr>`, captured at drag start. */
45
+ html: string;
46
+ /** Per-cell pixel widths (in DOM order) so the clone can fix them via `<col>`. */
47
+ cellWidths: number[];
48
+ /** Total row width — used as the wrapper width so the clone matches source horizontally. */
49
+ totalWidth: number;
50
+ /** Total row height — applied to the clone so cell padding matches the source row exactly. */
51
+ totalHeight: number;
52
+ };
53
+
54
+ /**
55
+ * Snapshot the source `<tr>` so the drag overlay can render a pixel-accurate
56
+ * floating clone. We can't reliably recompute the layout from `columns` alone
57
+ * — antd auto-sizes columns at runtime based on content + the surrounding
58
+ * container — so we read the rendered widths off the DOM at drag start. The
59
+ * row height is captured too because antd's cell padding rules are scoped to
60
+ * a selector chain we strip in the clone.
61
+ */
62
+ export function snapshotSourceRow(rowKey: string): RowSnapshot | null {
63
+ if (typeof document === 'undefined') return null;
64
+ // `CSS.escape` is in lib.dom and shipped in every browser we target; the
65
+ // guard is purely a belt-and-suspenders against exotic test environments
66
+ // where `window.CSS` may be absent.
67
+ const cssGlobal: { escape?: (value: string) => string } | undefined =
68
+ typeof window !== 'undefined' ? window.CSS : undefined;
69
+ const escaped = cssGlobal?.escape ? cssGlobal.escape(rowKey) : rowKey;
70
+ const sourceRow = document.querySelector(`tr[data-row-key="${escaped}"]`) as HTMLTableRowElement | null;
71
+ if (!sourceRow) return null;
72
+ const cellWidths = Array.from(sourceRow.cells).map((cell) => cell.getBoundingClientRect().width);
73
+ const rect = sourceRow.getBoundingClientRect();
74
+ return { html: sourceRow.outerHTML, cellWidths, totalWidth: rect.width, totalHeight: rect.height };
75
+ }
@@ -9,6 +9,8 @@
9
9
 
10
10
  export * from './AppComponents';
11
11
  export * from './BlankComponent';
12
+ export * from './form/table/dnd';
12
13
  export * from './form';
13
14
  export * from './Icon';
14
15
  export * from './RouterContextCleaner';
16
+ export * from './form/table';
@@ -9,8 +9,8 @@
9
9
 
10
10
  import { TinyColor } from '@ctrl/tinycolor';
11
11
  import { useEffect } from 'react';
12
- import { CustomToken, defaultTheme } from '@nocobase/client-v2';
13
12
  import { theme } from 'antd';
13
+ import { type CustomToken, defaultTheme } from '../theme';
14
14
 
15
15
  interface Result extends ReturnType<typeof theme.useToken> {
16
16
  token: CustomToken;
@@ -122,12 +122,11 @@ const FilterFormDefaultValuesUI = observer(
122
122
  rootCollection={getCollectionFromModel(ctx.model)}
123
123
  value={value}
124
124
  onChange={handleChange}
125
- fixedMode="default"
126
- showCondition={false}
127
125
  showValueEditorWhenNoField
128
126
  getValueInputProps={getValueInputProps}
129
127
  isTitleFieldCandidate={isTitleFieldCandidate}
130
128
  onSyncAssociationTitleField={onSyncAssociationTitleField}
129
+ enableDateVariableAsConstant
131
130
  />
132
131
  );
133
132
  },
@@ -681,6 +681,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
681
681
  AdminLayoutMenuItemModel.registerFlow({
682
682
  key: 'menuCreation',
683
683
  title: 'Add menu item',
684
+ manual: true,
684
685
  steps: {
685
686
  basic: {
686
687
  title: 'Add menu item',
@@ -697,6 +698,7 @@ AdminLayoutMenuItemModel.registerFlow({
697
698
  AdminLayoutMenuItemModel.registerFlow({
698
699
  key: 'menuSettings',
699
700
  title: 'Menu settings',
701
+ manual: true,
700
702
  steps: {
701
703
  edit: {
702
704
  title: 'Edit',
@@ -249,4 +249,115 @@ describe('resolveAdminRouteRuntimeTarget', () => {
249
249
  expect(toRouterNavigationPath('/nocobase/v2/admin/page-1', '/nocobase/v2')).toBe('/admin/page-1');
250
250
  expect(toRouterNavigationPath('/admin/page-1', '/nocobase/v2')).toBe('/admin/page-1');
251
251
  });
252
+
253
+ describe('v2 sub-app context (router basename contains /apps/<id>/)', () => {
254
+ const subApp = {
255
+ getPublicPath: () => '/nocobase/v2/',
256
+ router: {
257
+ getBasename: () => '/nocobase/v2/apps/test-app/',
258
+ },
259
+ } as any;
260
+
261
+ it('should resolve flowPage runtime path under sub-app basename', () => {
262
+ expect(
263
+ resolveAdminRouteRuntimeTarget({
264
+ app: subApp,
265
+ route: {
266
+ type: NocoBaseDesktopRouteType.flowPage,
267
+ schemaUid: 'fp1',
268
+ },
269
+ }),
270
+ ).toEqual({
271
+ runtimePath: '/nocobase/v2/apps/test-app/admin/fp1',
272
+ navigationMode: 'spa',
273
+ isLegacy: false,
274
+ reason: 'ok',
275
+ });
276
+ });
277
+
278
+ it('should resolve group DFS to first flowPage under sub-app basename', () => {
279
+ const route: NocoBaseDesktopRoute = {
280
+ type: NocoBaseDesktopRouteType.group,
281
+ children: [
282
+ {
283
+ type: NocoBaseDesktopRouteType.tabs,
284
+ schemaUid: 'tabs-1',
285
+ },
286
+ {
287
+ type: NocoBaseDesktopRouteType.group,
288
+ children: [
289
+ {
290
+ type: NocoBaseDesktopRouteType.page,
291
+ schemaUid: 'legacy-2',
292
+ },
293
+ {
294
+ type: NocoBaseDesktopRouteType.flowPage,
295
+ schemaUid: 'nested-fp',
296
+ },
297
+ ],
298
+ },
299
+ ],
300
+ };
301
+
302
+ expect(resolveAdminRouteRuntimeTarget({ app: subApp, route })).toEqual({
303
+ runtimePath: '/nocobase/v2/apps/test-app/admin/nested-fp',
304
+ navigationMode: 'spa',
305
+ isLegacy: false,
306
+ reason: 'ok',
307
+ });
308
+ });
309
+
310
+ it('should strip sub-app basename when converting to router internal path', () => {
311
+ expect(toRouterNavigationPath('/nocobase/v2/apps/test-app/admin/page-1', '/nocobase/v2/apps/test-app')).toBe(
312
+ '/admin/page-1',
313
+ );
314
+ expect(toRouterNavigationPath('/nocobase/v2/apps/test-app/admin/page-1', '/nocobase/v2/apps/test-app/')).toBe(
315
+ '/admin/page-1',
316
+ );
317
+ });
318
+ });
319
+
320
+ describe('fallback when router basename is missing', () => {
321
+ it('should fall back to publicPath when router is undefined', () => {
322
+ const appNoRouter = {
323
+ getPublicPath: () => '/nocobase/v2/',
324
+ router: undefined,
325
+ } as any;
326
+
327
+ expect(
328
+ resolveAdminRouteRuntimeTarget({
329
+ app: appNoRouter,
330
+ route: {
331
+ type: NocoBaseDesktopRouteType.flowPage,
332
+ schemaUid: 'fp1',
333
+ },
334
+ }),
335
+ ).toMatchObject({
336
+ runtimePath: '/nocobase/v2/admin/fp1',
337
+ reason: 'ok',
338
+ });
339
+ });
340
+
341
+ it('should fall back to publicPath when getBasename returns undefined', () => {
342
+ const appNoBasename = {
343
+ getPublicPath: () => '/nocobase/v2/',
344
+ router: {
345
+ getBasename: () => undefined,
346
+ },
347
+ } as any;
348
+
349
+ expect(
350
+ resolveAdminRouteRuntimeTarget({
351
+ app: appNoBasename,
352
+ route: {
353
+ type: NocoBaseDesktopRouteType.flowPage,
354
+ schemaUid: 'fp1',
355
+ },
356
+ }),
357
+ ).toMatchObject({
358
+ runtimePath: '/nocobase/v2/admin/fp1',
359
+ reason: 'ok',
360
+ });
361
+ });
362
+ });
252
363
  });
@@ -7,6 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
+ import { getV2EffectiveBasePath } from '../../../authRedirect';
10
11
  import type { BaseApplication } from '../../../BaseApplication';
11
12
  import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../flow-compat';
12
13
 
@@ -106,7 +107,7 @@ function joinRootRelativePath(basePath: string, pathname: string) {
106
107
  }
107
108
 
108
109
  function getV2AdminPath(app: ResolveAdminRouteRuntimeTargetOptions['app'], schemaUid: string) {
109
- return joinRootRelativePath(app.getPublicPath(), `/admin/${schemaUid}`);
110
+ return joinRootRelativePath(getV2EffectiveBasePath(app), `/admin/${schemaUid}`);
110
111
  }
111
112
 
112
113
  function appendLocationState(pathname: string, location?: LocationLike) {
@@ -20,7 +20,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
20
20
  import { Button, Input } from 'antd';
21
21
  import { css } from '@emotion/css';
22
22
  import type { TextAreaRef } from 'antd/es/input/TextArea';
23
- import { FlowContextSelector, useFlowContext } from '@nocobase/flow-engine';
23
+ import { FlowContextSelector, useFlowContext, type MetaTreeNode } from '@nocobase/flow-engine';
24
24
 
25
25
  export interface TextAreaWithContextSelectorProps {
26
26
  value?: string;
@@ -29,6 +29,20 @@ export interface TextAreaWithContextSelectorProps {
29
29
  rows?: number;
30
30
  maxRows?: number;
31
31
  style?: React.CSSProperties;
32
+ disabled?: boolean;
33
+ /**
34
+ * Custom meta tree for the variable picker. Accepts an array, a sync getter,
35
+ * or an async getter — same shape as `FlowContextSelector`'s `metaTree`. If
36
+ * omitted, the full `ctx.getPropertyMetaTree()` is used (legacy default).
37
+ */
38
+ metaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
39
+ /**
40
+ * Format a picked meta node into the string inserted at the caret. When
41
+ * omitted, the FlowContextSelector default (`{{ ctx.X.Y }}`) is used.
42
+ * Override to match a different storage convention — e.g. NocoBase server
43
+ * templates use `{{$X.Y}}` without the `ctx.` prefix.
44
+ */
45
+ formatPathToValue?: (meta: MetaTreeNode) => string;
32
46
  }
33
47
 
34
48
  /**
@@ -41,6 +55,9 @@ export const TextAreaWithContextSelector: React.FC<TextAreaWithContextSelectorPr
41
55
  rows = 3,
42
56
  maxRows = 24,
43
57
  style,
58
+ disabled,
59
+ metaTree,
60
+ formatPathToValue,
44
61
  }) => {
45
62
  const flowCtx = useFlowContext();
46
63
  const [innerValue, setInnerValue] = useState<string>(value || '');
@@ -76,10 +93,11 @@ export const TextAreaWithContextSelector: React.FC<TextAreaWithContextSelectorPr
76
93
  const next = prev.slice(0, start) + toInsert + prev.slice(end);
77
94
  setInnerValue(next);
78
95
  onChange?.(next);
79
- // 恢复光标位置并聚焦
96
+ // 插入后选中刚插入的变量文本,与 v1 RawTextArea 行为一致:
97
+ // 用户可立即按删除键移除整段变量,或继续输入直接替换。
80
98
  requestAnimationFrame(() => {
81
99
  const pos = start + (toInsert?.length || 0);
82
- el.setSelectionRange(pos, pos);
100
+ el.setSelectionRange(start, pos);
83
101
  el.focus();
84
102
  });
85
103
  },
@@ -94,8 +112,8 @@ export const TextAreaWithContextSelector: React.FC<TextAreaWithContextSelectorPr
94
112
  [insertAtCaret],
95
113
  );
96
114
 
97
- // 使用函数形式提供变量树,保证与运行时上下文一致
98
- const metaTree = useMemo(() => () => flowCtx.getPropertyMetaTree?.(), [flowCtx]);
115
+ // 使用函数形式提供变量树,保证与运行时上下文一致;当外部传入则尊重外部值。
116
+ const resolvedMetaTree = useMemo(() => metaTree ?? (() => flowCtx.getPropertyMetaTree?.()), [flowCtx, metaTree]);
99
117
 
100
118
  return (
101
119
  <div style={{ position: 'relative', width: '100%', ...style }}>
@@ -105,6 +123,7 @@ export const TextAreaWithContextSelector: React.FC<TextAreaWithContextSelectorPr
105
123
  onChange={handleTextChange}
106
124
  autoSize={{ minRows: rows, maxRows }}
107
125
  placeholder={placeholder}
126
+ disabled={disabled}
108
127
  style={{ width: '100%' }}
109
128
  />
110
129
  <div
@@ -116,7 +135,12 @@ export const TextAreaWithContextSelector: React.FC<TextAreaWithContextSelectorPr
116
135
  lineHeight: 0,
117
136
  }}
118
137
  >
119
- <FlowContextSelector metaTree={metaTree} onChange={(val) => handleVariableSelected(val)}>
138
+ <FlowContextSelector
139
+ metaTree={resolvedMetaTree}
140
+ disabled={disabled}
141
+ formatPathToValue={formatPathToValue}
142
+ onChange={(val) => handleVariableSelected(val)}
143
+ >
120
144
  <Button
121
145
  type="default"
122
146
  style={{ fontStyle: 'italic', fontFamily: 'New York, Times New Roman, Times, serif' }}