@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
@@ -12,9 +12,10 @@ import { describe, expect, it, vi } from 'vitest';
12
12
  import { render, waitFor } from '@nocobase/test/client';
13
13
  import { FieldAssignValueInput } from '../FieldAssignValueInput';
14
14
 
15
- const { mockUseFlowContext, mockGetDefaultBindingByField } = vi.hoisted(() => ({
15
+ const { mockUseFlowContext, mockGetDefaultBindingByField, mockVariableInput } = vi.hoisted(() => ({
16
16
  mockUseFlowContext: vi.fn(),
17
17
  mockGetDefaultBindingByField: vi.fn(),
18
+ mockVariableInput: vi.fn(() => <div data-testid="variable-input" />),
18
19
  }));
19
20
 
20
21
  vi.mock('@nocobase/flow-engine', async () => {
@@ -25,7 +26,7 @@ vi.mock('@nocobase/flow-engine', async () => {
25
26
  return {
26
27
  ...actual,
27
28
  useFlowContext: () => mockUseFlowContext(),
28
- VariableInput: () => <div data-testid="variable-input" />,
29
+ VariableInput: mockVariableInput,
29
30
  FlowModelRenderer: () => <div data-testid="flow-model-renderer" />,
30
31
  EditableItemModel: MockEditableItemModel,
31
32
  };
@@ -131,4 +132,85 @@ describe('FieldAssignValueInput context', () => {
131
132
  });
132
133
  expect(engine.createModel).toHaveBeenCalledWith(expect.any(Object), { delegate: sourceContext });
133
134
  });
135
+
136
+ it('lets callers override variable path parsing for domain-specific stored formats', async () => {
137
+ mockVariableInput.mockClear();
138
+ const sourceContext = {
139
+ dataSourceManager: {
140
+ getDataSource: vi.fn(() => ({})),
141
+ },
142
+ t: (key: string) => key,
143
+ };
144
+ const fieldModel = {
145
+ props: {},
146
+ setProps: vi.fn(),
147
+ dispatchEvent: vi.fn(),
148
+ remove: vi.fn(),
149
+ };
150
+ const tempRoot = {
151
+ context: {
152
+ defineProperty: vi.fn(),
153
+ },
154
+ subModels: {
155
+ fields: [fieldModel],
156
+ },
157
+ setProps: vi.fn(),
158
+ remove: vi.fn(),
159
+ };
160
+ const engine = {
161
+ createModel: vi.fn(() => tempRoot),
162
+ };
163
+ const collectionField = {
164
+ name: 'status',
165
+ interface: 'input',
166
+ uiSchema: { 'x-component': 'Input' },
167
+ isAssociationField: () => false,
168
+ getComponentProps: () => ({}),
169
+ };
170
+ const collection = {
171
+ dataSourceKey: 'main',
172
+ name: 'tasks',
173
+ getField: (name: string) => (name === 'status' ? collectionField : null),
174
+ getFields: () => [collectionField],
175
+ };
176
+ const formModel = {
177
+ context: { ...sourceContext, engine, collection, blockModel: null as any },
178
+ collection,
179
+ subModels: {},
180
+ };
181
+ formModel.context.blockModel = formModel;
182
+
183
+ mockGetDefaultBindingByField.mockReturnValue({ modelName: 'InputFieldModel' });
184
+ mockUseFlowContext.mockReturnValue({
185
+ model: formModel,
186
+ t: (key: string) => key,
187
+ getPropertyMetaTree: vi.fn(async () => []),
188
+ });
189
+
190
+ const variableConverters = {
191
+ resolvePathFromValue: vi.fn((value: string) =>
192
+ value === '{{$context.data.updatedAt}}' ? ['$context', 'data', 'updatedAt'] : undefined,
193
+ ),
194
+ resolveValueFromPath: vi.fn(() => undefined),
195
+ };
196
+
197
+ render(
198
+ <FieldAssignValueInput
199
+ targetPath="status"
200
+ value="{{$context.data.updatedAt}}"
201
+ onChange={vi.fn()}
202
+ variableConverters={variableConverters}
203
+ />,
204
+ );
205
+
206
+ await waitFor(() => {
207
+ expect(engine.createModel).toHaveBeenCalled();
208
+ });
209
+ const latestVariableInputCall = mockVariableInput.mock.calls.at(-1)?.[0];
210
+ expect(latestVariableInputCall.converters.resolvePathFromValue('{{$context.data.updatedAt}}')).toEqual([
211
+ '$context',
212
+ 'data',
213
+ 'updatedAt',
214
+ ]);
215
+ });
134
216
  });
@@ -20,8 +20,6 @@ import {
20
20
  FlowEngineProvider,
21
21
  FlowModelRenderer,
22
22
  ElementProxy,
23
- createSafeWindow,
24
- createSafeDocument,
25
23
  createViewScopedEngine,
26
24
  } from '@nocobase/flow-engine';
27
25
  import { JSEditableFieldModel } from '../../../models/fields/JSEditableFieldModel';
@@ -167,11 +165,7 @@ describe('useCodeRunner (beforeRender)', () => {
167
165
  const code = ctx?.inputArgs?.preview?.code || '';
168
166
  ctx.onRefReady(ctx.ref, async (el) => {
169
167
  ctx.defineProperty('element', { get: () => new ElementProxy(el as any) });
170
- await ctx.runjs(
171
- code,
172
- { window: createSafeWindow(), document: createSafeDocument() },
173
- { preprocessTemplates: true },
174
- );
168
+ await ctx.runjs(code, undefined, { preprocessTemplates: true });
175
169
  });
176
170
  },
177
171
  },
@@ -286,16 +280,7 @@ return currentUsername;
286
280
  const code = ctx?.inputArgs?.preview?.code || '';
287
281
  ctx.onRefReady(ctx.ref, async (el) => {
288
282
  ctx.defineProperty('element', { get: () => new ElementProxy(el as any) });
289
- const navigator = { userAgent: 'test' } as any;
290
- await ctx.runjs(
291
- code,
292
- {
293
- window: createSafeWindow({ navigator }),
294
- document: createSafeDocument(),
295
- navigator,
296
- },
297
- { preprocessTemplates: true },
298
- );
283
+ await ctx.runjs(code, undefined, { preprocessTemplates: true });
299
284
  });
300
285
  },
301
286
  },
@@ -9,14 +9,7 @@
9
9
 
10
10
  import { useCallback, useRef, useState } from 'react';
11
11
  import { parseErrorLineColumn, WRAPPER_PRELUDE_LINES } from '../errorHelpers';
12
- import {
13
- FlowModelContext,
14
- JSRunner,
15
- createSafeWindow,
16
- createSafeDocument,
17
- createSafeNavigator,
18
- prepareRunJsCode,
19
- } from '@nocobase/flow-engine';
12
+ import { FlowModelContext, JSRunner, prepareRunJsCode } from '@nocobase/flow-engine';
20
13
 
21
14
  export type RunLog = { level: 'log' | 'info' | 'warn' | 'error'; msg: string; line?: number; column?: number };
22
15
  type RunResult = Awaited<ReturnType<JSRunner['run']>>;
@@ -286,12 +279,7 @@ export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
286
279
  const eventName = typeof onDef === 'string' ? onDef : onDef?.eventName;
287
280
  if (!flow) {
288
281
  // 无可用流程(典型场景:联动规则里的 RunJS 预览),直接在当前上下文执行代码
289
- const navigator = createSafeNavigator();
290
- await directRunCtx.runjs(
291
- code,
292
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
293
- { version },
294
- );
282
+ await directRunCtx.runjs(code, undefined, { version });
295
283
  } else if (typeof eventName === 'string') {
296
284
  await m.dispatchEvent(eventName, { preview: { code } }, { sequential: true, useCache: false });
297
285
  } else if (isManual) {
@@ -20,10 +20,8 @@ import * as acornWalk from 'acorn-walk';
20
20
  import {
21
21
  JSRunner,
22
22
  FlowContext,
23
- createSafeDocument,
24
- createSafeNavigator,
25
- createSafeWindow,
26
23
  prepareRunJsCode,
24
+ RUNJS_ALLOWED_BARE_GLOBAL_NAMES,
27
25
  shouldPreprocessRunJSTemplates,
28
26
  } from '@nocobase/flow-engine';
29
27
 
@@ -604,41 +602,7 @@ function collectHeuristicIssues(code: string): RunJSIssue[] {
604
602
  return issues;
605
603
  }
606
604
 
607
- const declared = new Set<string>([
608
- // Common globals / allowed runtime context roots
609
- 'ctx',
610
- 'console',
611
- 'window',
612
- 'document',
613
- 'navigator',
614
- 'Math',
615
- 'Date',
616
- 'Array',
617
- 'Object',
618
- 'Number',
619
- 'String',
620
- 'Boolean',
621
- 'Promise',
622
- 'RegExp',
623
- 'Set',
624
- 'Map',
625
- 'WeakSet',
626
- 'WeakMap',
627
- 'JSON',
628
- 'Intl',
629
- 'URL',
630
- 'Error',
631
- 'TypeError',
632
- 'encodeURIComponent',
633
- 'decodeURIComponent',
634
- 'parseInt',
635
- 'parseFloat',
636
- 'isNaN',
637
- 'isFinite',
638
- 'undefined',
639
- 'NaN',
640
- 'Infinity',
641
- ]);
605
+ const declared = new Set<string>(RUNJS_ALLOWED_BARE_GLOBAL_NAMES);
642
606
 
643
607
  const addId = (id: any) => {
644
608
  if (id && typeof id.name === 'string') declared.add(id.name);
@@ -1036,15 +1000,14 @@ export async function diagnoseRunJS(
1036
1000
  const { consoleCapture, loggerCapture } = createLogCollectors(logs);
1037
1001
 
1038
1002
  const baseGlobals: Record<string, any> = { console: consoleCapture };
1039
- try {
1040
- if (typeof window !== 'undefined') {
1041
- const navigator = createSafeNavigator();
1003
+ if (typeof window !== 'undefined') {
1004
+ baseGlobals.window = window;
1005
+ if (typeof navigator !== 'undefined') {
1042
1006
  baseGlobals.navigator = navigator;
1043
- baseGlobals.window = createSafeWindow({ navigator });
1044
- baseGlobals.document = createSafeDocument();
1045
1007
  }
1046
- } catch (_) {
1047
- // ignore safe globals failures
1008
+ }
1009
+ if (typeof document !== 'undefined') {
1010
+ baseGlobals.document = document;
1048
1011
  }
1049
1012
 
1050
1013
  let prepared = src;
@@ -45,6 +45,7 @@ interface FilterGroupProps {
45
45
  value: Record<string, any>;
46
46
  /** 自定义筛选项组件 */
47
47
  FilterItem?: React.FC<FilterItemProps>;
48
+ disabled?: boolean;
48
49
  closeIcon?: ReactNode;
49
50
  /** 是否显示边框 */
50
51
  showBorder?: boolean;
@@ -89,6 +90,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
89
90
  const {
90
91
  value = { logic: '$and', items: [] },
91
92
  FilterItem,
93
+ disabled = false,
92
94
  showBorder = false,
93
95
  onRemove,
94
96
  onChange,
@@ -120,11 +122,17 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
120
122
  };
121
123
 
122
124
  const handleLogicChange = (newLogic: '$and' | '$or') => {
125
+ if (disabled) {
126
+ return;
127
+ }
123
128
  value.logic = newLogic;
124
129
  onChange?.(value);
125
130
  };
126
131
 
127
132
  const handleAddCondition = () => {
133
+ if (disabled) {
134
+ return;
135
+ }
128
136
  items.push({
129
137
  path: '',
130
138
  operator: '',
@@ -134,6 +142,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
134
142
  };
135
143
 
136
144
  const handleAddConditionGroup = () => {
145
+ if (disabled) {
146
+ return;
147
+ }
137
148
  items.push({
138
149
  logic: '$and',
139
150
  items: [],
@@ -142,6 +153,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
142
153
  };
143
154
 
144
155
  const handleRemoveItem = (index: number) => {
156
+ if (disabled) {
157
+ return;
158
+ }
145
159
  items.splice(index, 1);
146
160
  onChange?.(value);
147
161
  };
@@ -160,11 +174,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
160
174
  <button
161
175
  type="button"
162
176
  aria-label="icon-close"
177
+ disabled={disabled}
163
178
  style={{
164
179
  position: 'absolute',
165
180
  right: 10,
166
181
  top: 10,
167
- cursor: 'pointer',
182
+ cursor: disabled ? 'not-allowed' : 'pointer',
168
183
  background: 'transparent',
169
184
  border: 0,
170
185
  padding: 0,
@@ -182,6 +197,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
182
197
  data-testid="filter-select-all-or-any"
183
198
  style={{ width: 'auto' }}
184
199
  value={logic}
200
+ disabled={disabled}
185
201
  onChange={handleLogicChange}
186
202
  >
187
203
  <Select.Option value="$and">{t('All')}</Select.Option>
@@ -200,6 +216,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
200
216
  key={getFilterItemKey(item)}
201
217
  value={item}
202
218
  FilterItem={FilterItem}
219
+ disabled={disabled}
203
220
  showBorder={true}
204
221
  onRemove={() => handleRemoveItem(index)}
205
222
  onChange={(v) => {
@@ -221,11 +238,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
221
238
  <button
222
239
  type="button"
223
240
  aria-label="icon-close"
241
+ disabled={disabled}
224
242
  style={{
225
243
  marginLeft: 8,
226
244
  marginRight: 8,
227
245
  flex: '0 0 auto',
228
- cursor: 'pointer',
246
+ cursor: disabled ? 'not-allowed' : 'pointer',
229
247
  background: 'transparent',
230
248
  border: 0,
231
249
  padding: 0,
@@ -251,11 +269,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
251
269
  <button
252
270
  type="button"
253
271
  aria-label="icon-close"
272
+ disabled={disabled}
254
273
  style={{
255
274
  marginLeft: 8,
256
275
  marginRight: 8,
257
276
  flex: '0 0 auto',
258
- cursor: 'pointer',
277
+ cursor: disabled ? 'not-allowed' : 'pointer',
259
278
  background: 'transparent',
260
279
  border: 0,
261
280
  padding: 0,
@@ -277,11 +296,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
277
296
  <button
278
297
  type="button"
279
298
  aria-label="icon-close"
299
+ disabled={disabled}
280
300
  style={{
281
301
  marginLeft: 8,
282
302
  marginRight: 8,
283
303
  flex: '0 0 auto',
284
- cursor: 'pointer',
304
+ cursor: disabled ? 'not-allowed' : 'pointer',
285
305
  background: 'transparent',
286
306
  border: 0,
287
307
  padding: 0,
@@ -298,7 +318,14 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
298
318
  </div>
299
319
 
300
320
  <Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
301
- <Button style={{ padding: 0 }} type="link" size="small" icon={<PlusOutlined />} onClick={handleAddCondition}>
321
+ <Button
322
+ style={{ padding: 0 }}
323
+ type="link"
324
+ size="small"
325
+ icon={<PlusOutlined />}
326
+ onClick={handleAddCondition}
327
+ disabled={disabled}
328
+ >
302
329
  {t('Add condition')}
303
330
  </Button>
304
331
  <Button
@@ -307,6 +334,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
307
334
  size="small"
308
335
  icon={<PlusOutlined />}
309
336
  onClick={handleAddConditionGroup}
337
+ disabled={disabled}
310
338
  >
311
339
  {t('Add condition group')}
312
340
  </Button>
@@ -62,6 +62,51 @@ async function buildCollectionLeftMetaTreeLocal(ctx: any): Promise<MetaTreeNode[
62
62
  return await resolve(subTree);
63
63
  }
64
64
 
65
+ function buildCollectionFieldMetaNode(collection: any, path: string[]): MetaTreeNode | null {
66
+ if (!collection || !path.length) {
67
+ return null;
68
+ }
69
+
70
+ const normalizedPath = path[0] === 'collection' ? path.slice(1) : path;
71
+ if (!normalizedPath.length) {
72
+ return null;
73
+ }
74
+
75
+ const parentTitles: string[] = [];
76
+ let currentCollection = collection;
77
+ let currentNode: MetaTreeNode | null = null;
78
+
79
+ for (const segment of normalizedPath) {
80
+ const field =
81
+ currentCollection?.getField?.(segment) ??
82
+ currentCollection?.getFields?.()?.find?.((candidate: any) => candidate?.name === segment);
83
+ if (!field) {
84
+ return null;
85
+ }
86
+
87
+ const title = field.title || field.uiSchema?.title || field.name || segment;
88
+ currentNode = {
89
+ name: field.name || segment,
90
+ title,
91
+ type: field.type,
92
+ interface: field.interface,
93
+ uiSchema: field.uiSchema,
94
+ options: field.options,
95
+ paths: ['collection', ...normalizedPath.slice(0, parentTitles.length + 1)],
96
+ parentTitles: [...parentTitles],
97
+ } as MetaTreeNode;
98
+
99
+ if (field.targetCollection) {
100
+ parentTitles.push(title);
101
+ currentCollection = field.targetCollection;
102
+ } else {
103
+ currentCollection = null;
104
+ }
105
+ }
106
+
107
+ return currentNode;
108
+ }
109
+
65
110
  export interface VariableFilterItemValue {
66
111
  path: string;
67
112
  operator: string;
@@ -75,6 +120,7 @@ export interface VariableFilterItemProps {
75
120
  /** 筛选条件值对象 */
76
121
  value: VariableFilterItemValue;
77
122
  model: FlowModel;
123
+ disabled?: boolean;
78
124
  /**
79
125
  * 是否启用右侧 VariableInput(变量或静态值二合一)。
80
126
  * 默认 false:保持原有行为,右侧仅静态输入组件。
@@ -85,6 +131,10 @@ export interface VariableFilterItemProps {
85
131
  * 默认使用整棵 ctx 的 metaTree:model.context.getPropertyMetaTree()
86
132
  */
87
133
  rightMetaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
134
+ /**
135
+ * 右侧变量的领域转换器。常量和空值仍由 VariableFilterItem 处理,未匹配的值回退到 FlowEngine 默认转换器。
136
+ */
137
+ rightVariableConverters?: Pick<Converters, 'resolvePathFromValue' | 'resolveValueFromPath'>;
88
138
  ignoreFieldNames?: string[];
89
139
  maxAssociationFieldDepth?: number;
90
140
  }
@@ -265,11 +315,57 @@ function normalizeRightValueInput(input: unknown) {
265
315
  return input;
266
316
  }
267
317
 
318
+ function findMetaTreeNodeByPath(metaTree: MetaTreeNode[], targetPath: string[]): MetaTreeNode | null {
319
+ if (!Array.isArray(metaTree) || !targetPath.length) {
320
+ return null;
321
+ }
322
+
323
+ const traverseByName = (nodes: MetaTreeNode[], path: string[]): MetaTreeNode | null => {
324
+ if (!path.length) {
325
+ return null;
326
+ }
327
+
328
+ const [head, ...rest] = path;
329
+ const matched = nodes.find((node) => String(node.name) === String(head));
330
+ if (!matched) {
331
+ return null;
332
+ }
333
+ if (!rest.length) {
334
+ return matched;
335
+ }
336
+ if (!Array.isArray(matched.children)) {
337
+ return null;
338
+ }
339
+ return traverseByName(matched.children as MetaTreeNode[], rest);
340
+ };
341
+
342
+ const byNames = traverseByName(metaTree, targetPath);
343
+ if (byNames) {
344
+ return byNames;
345
+ }
346
+
347
+ const topNames = new Set(metaTree.map((node) => String(node.name)));
348
+ if (!topNames.has(String(targetPath[0]))) {
349
+ return traverseByName(metaTree, targetPath.slice(1));
350
+ }
351
+
352
+ return null;
353
+ }
354
+
268
355
  /**
269
356
  * 上下文筛选项组件
270
357
  */
271
358
  export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
272
- ({ value, model, rightAsVariable, rightMetaTree, ignoreFieldNames, maxAssociationFieldDepth }) => {
359
+ ({
360
+ value,
361
+ model,
362
+ disabled = false,
363
+ rightAsVariable,
364
+ rightMetaTree,
365
+ rightVariableConverters,
366
+ ignoreFieldNames,
367
+ maxAssociationFieldDepth,
368
+ }) => {
273
369
  // 使用 View 上下文,确保可访问 ctx.view 的异步子树
274
370
  const ctx = useFlowViewContext();
275
371
  const t = model.translate;
@@ -334,6 +430,9 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
334
430
  // 处理左侧值变化(值由 converters 决定如何解析)
335
431
  const handleLeftChange = useCallback(
336
432
  (variableValue: string, meta?: MetaTreeNode) => {
433
+ if (disabled) {
434
+ return;
435
+ }
337
436
  const prevPath = value.path || '';
338
437
  const nextPath = variableValue || '';
339
438
  const changed = nextPath !== prevPath;
@@ -348,7 +447,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
348
447
  setLeftMeta(meta);
349
448
  }
350
449
  },
351
- [value],
450
+ [disabled, value],
352
451
  );
353
452
 
354
453
  // 自定义转换器来捕获 MetaTreeNode,并在选择左值时设置默认操作符
@@ -376,6 +475,9 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
376
475
  // 处理操作符变化
377
476
  const handleOperatorChange = useCallback(
378
477
  (operatorValue: string) => {
478
+ if (disabled) {
479
+ return;
480
+ }
379
481
  value.operator = operatorValue;
380
482
  const cur = operatorMetaList.find((op) => op.value === operatorValue);
381
483
  if (cur?.noValue) {
@@ -386,7 +488,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
386
488
  value.noValue = false;
387
489
  }
388
490
  },
389
- [operatorMetaList, value],
491
+ [disabled, operatorMetaList, value],
390
492
  );
391
493
 
392
494
  // 使用公共静态输入渲染器(抽取到 utils)
@@ -464,9 +566,12 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
464
566
 
465
567
  const setRightValue = useCallback(
466
568
  (next: unknown) => {
569
+ if (disabled) {
570
+ return;
571
+ }
467
572
  value.value = normalizeRightValueInput(next) as VariableFilterItemValue['value'];
468
573
  },
469
- [value],
574
+ [disabled, value],
470
575
  );
471
576
 
472
577
  // 右侧静态输入(无变量模式)与右侧 VariableInput 的静态渲染组件,统一复用
@@ -520,6 +625,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
520
625
  {...nextProps}
521
626
  style={{ width: '100%', ...pickStyle(nextProps.style) }}
522
627
  value={normalized}
628
+ disabled={disabled}
523
629
  onChange={(vals: any) => setRightValue(vals)}
524
630
  />
525
631
  </div>
@@ -529,10 +635,19 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
529
635
  const Comp = staticInputRenderer;
530
636
  return (
531
637
  <div style={{ flex: '1 1 40%', minWidth: 160, maxWidth: '100%' }}>
532
- <Comp value={rightValue} onChange={(val) => setRightValue(val)} />
638
+ <Comp value={rightValue} onChange={(val) => setRightValue(val)} disabled={disabled} />
533
639
  </div>
534
640
  );
535
- }, [operator, operatorMetaList, rightValue, staticInputRenderer, model.context.app, setRightValue, enumOptions]);
641
+ }, [
642
+ disabled,
643
+ operator,
644
+ operatorMetaList,
645
+ rightValue,
646
+ staticInputRenderer,
647
+ model.context.app,
648
+ setRightValue,
649
+ enumOptions,
650
+ ]);
536
651
 
537
652
  // Null 占位组件(仿照 DefaultValue.tsx 的实现)
538
653
  const NullComponent = useMemo(() => {
@@ -582,17 +697,19 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
582
697
  const first = meta?.paths?.[0];
583
698
  if (first === 'constant') return '';
584
699
  if (first === 'null') return null;
585
- return undefined; // 交给默认逻辑格式化变量表达式
700
+ return rightVariableConverters?.resolveValueFromPath?.(meta);
586
701
  },
587
702
  resolvePathFromValue: (val) => {
588
703
  if (val === null) return ['null'];
589
- // 变量表达式:使用内置解析;其他静态值走 constant
590
- const parsed = typeof val === 'string' ? parseValueToPath(val) : undefined;
704
+ // 变量表达式:优先使用领域转换器,再回退内置解析;其他静态值走 constant
705
+ const parsed =
706
+ rightVariableConverters?.resolvePathFromValue?.(val) ??
707
+ (typeof val === 'string' ? parseValueToPath(val) : undefined);
591
708
  if (parsed) return parsed;
592
709
  return ['constant'];
593
710
  },
594
711
  };
595
- }, [NullComponent, staticInputRenderer]);
712
+ }, [NullComponent, rightVariableConverters, staticInputRenderer]);
596
713
 
597
714
  // 为 2.0 左侧字段选择器追加“接口 filterable.children”定义(如 chinaRegion 的“省市区名称”子项),
598
715
  // 以恢复 1.0 左侧子菜单的能力。
@@ -652,6 +769,34 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
652
769
  };
653
770
  }, [getFieldInterface, model, maxAssociationFieldDepth]);
654
771
 
772
+ useEffect(() => {
773
+ if (leftMeta || !value.path) {
774
+ return;
775
+ }
776
+
777
+ let cancelled = false;
778
+ const restore = async () => {
779
+ const nodes = await enhancedMetaTree();
780
+ if (cancelled) {
781
+ return;
782
+ }
783
+ const path = customConverters.resolvePathFromValue(value.path);
784
+ if (!path?.length) {
785
+ return;
786
+ }
787
+ const matched =
788
+ findMetaTreeNodeByPath(nodes, path) || buildCollectionFieldMetaNode(model.context.collection, path);
789
+ if (matched) {
790
+ setLeftMeta(matched);
791
+ }
792
+ };
793
+
794
+ restore();
795
+ return () => {
796
+ cancelled = true;
797
+ };
798
+ }, [customConverters, enhancedMetaTree, leftMeta, model.context.collection, value.path]);
799
+
655
800
  return (
656
801
  <Space wrap style={{ width: '100%' }}>
657
802
  <VariableInput
@@ -659,6 +804,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
659
804
  metaTree={enhancedMetaTree}
660
805
  onChange={handleLeftChange}
661
806
  converters={customConverters}
807
+ disabled={disabled}
662
808
  showValueComponent={false}
663
809
  style={{ flex: '1 1 40%', minWidth: 160, maxWidth: '100%' }}
664
810
  onlyLeafSelectable={true}
@@ -670,6 +816,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
670
816
  style={{ flex: '0 0 140px', minWidth: 120, maxWidth: '100%' }}
671
817
  placeholder={t('Comparison')}
672
818
  value={operator || undefined}
819
+ disabled={disabled}
673
820
  onChange={handleOperatorChange}
674
821
  >
675
822
  {operatorOptions.map((op) => (
@@ -686,6 +833,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
686
833
  onChange={(v) => setRightValue(v)}
687
834
  metaTree={mergedRightMetaTree}
688
835
  converters={rightConverters}
836
+ disabled={disabled}
689
837
  showValueComponent
690
838
  style={{ flex: '1 1 40%', minWidth: 160, maxWidth: '100%' }}
691
839
  placeholder={t('Enter value')}