@nocobase/client-v2 2.1.0-alpha.45 → 2.1.0-alpha.47

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 (99) hide show
  1. package/es/authRedirect.d.ts +12 -0
  2. package/es/collection-manager/filter-operators/index.d.ts +1 -0
  3. package/es/components/form/TypedVariableInput.d.ts +75 -0
  4. package/es/components/form/filter/CollectionFilterItem.d.ts +5 -1
  5. package/es/components/form/filter/FilterValueInput.d.ts +5 -0
  6. package/es/components/form/index.d.ts +1 -0
  7. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  8. package/es/flow/components/ConditionBuilder.d.ts +1 -0
  9. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  10. package/es/flow/components/FieldAssignValueInput.d.ts +1 -0
  11. package/es/flow/components/fieldAssignOptions.d.ts +10 -0
  12. package/es/flow/components/filter/LinkageFilterItem.d.ts +1 -0
  13. package/es/flow/components/filter/VariableFilterItem.d.ts +1 -0
  14. package/es/flow/components/filter/metaTreeAssociationDepth.d.ts +13 -0
  15. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +26 -3
  16. package/es/flow/models/base/AssociationFieldGroupModel.d.ts +2 -33
  17. package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +8 -0
  18. package/es/flow/models/blocks/filter-manager/utils.d.ts +23 -1
  19. package/es/flow/models/blocks/table/TableColumnModel.d.ts +2 -0
  20. package/es/index.mjs +106 -106
  21. package/lib/index.js +106 -106
  22. package/package.json +7 -7
  23. package/src/__tests__/browserChecker.test.ts +103 -0
  24. package/src/__tests__/dataSourceRuntime.test.tsx +22 -1
  25. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +5 -0
  26. package/src/authRedirect.ts +43 -0
  27. package/src/collection-field-interface/CollectionFieldInterface.ts +14 -7
  28. package/src/collection-manager/filter-operators/index.ts +29 -2
  29. package/src/components/README.md +48 -0
  30. package/src/components/README.zh-CN.md +48 -0
  31. package/src/components/form/TypedVariableInput.tsx +441 -0
  32. package/src/components/form/__tests__/TypedVariableInput.test.tsx +152 -0
  33. package/src/components/form/filter/CollectionFilterItem.tsx +10 -2
  34. package/src/components/form/filter/FilterValueInput.tsx +27 -2
  35. package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +39 -0
  36. package/src/components/form/index.tsx +1 -0
  37. package/src/flow/__tests__/FlowRoute.test.tsx +8 -1
  38. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +100 -0
  39. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +369 -2
  40. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +23 -1
  41. package/src/flow/actions/dataScope.tsx +8 -1
  42. package/src/flow/actions/linkageRules.tsx +61 -5
  43. package/src/flow/actions/linkageRulesRefresh.tsx +2 -3
  44. package/src/flow/actions/setTargetDataScope.tsx +1 -1
  45. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +15 -1
  46. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +75 -1
  47. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +11 -0
  48. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -4
  49. package/src/flow/common/Markdown/Edit.tsx +32 -4
  50. package/src/flow/components/ConditionBuilder.tsx +12 -2
  51. package/src/flow/components/DefaultValue.tsx +14 -8
  52. package/src/flow/components/FieldAssignRulesEditor.tsx +37 -46
  53. package/src/flow/components/FieldAssignValueInput.tsx +14 -14
  54. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +112 -1
  55. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +71 -1
  56. package/src/flow/components/code-editor/__tests__/runjsCompletions.test.ts +272 -27
  57. package/src/flow/components/code-editor/runjsCompletions.ts +490 -9
  58. package/src/flow/components/fieldAssignOptions.ts +23 -6
  59. package/src/flow/components/filter/LinkageFilterItem.tsx +26 -12
  60. package/src/flow/components/filter/VariableFilterItem.tsx +32 -13
  61. package/src/flow/components/filter/__tests__/fieldsToOptions.test.ts +48 -0
  62. package/src/flow/components/filter/__tests__/metaTreeAssociationDepth.test.ts +60 -0
  63. package/src/flow/components/filter/fieldsToOptions.ts +9 -4
  64. package/src/flow/components/filter/metaTreeAssociationDepth.ts +69 -0
  65. package/src/flow/internal/utils/operatorSchemaHelper.ts +112 -5
  66. package/src/flow/models/actions/FilterActionModel.tsx +15 -3
  67. package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +90 -0
  68. package/src/flow/models/base/AssociationFieldGroupModel.tsx +20 -3
  69. package/src/flow/models/base/GridModel.tsx +1 -1
  70. package/src/flow/models/base/PageModel/ChildPageModel.tsx +5 -1
  71. package/src/flow/models/base/PageModel/__tests__/ChildPageModel.test.ts +12 -0
  72. package/src/flow/models/base/__tests__/AssociationFieldGroupModel.test.ts +86 -0
  73. package/src/flow/models/base/__tests__/GridModel.render.test.tsx +53 -0
  74. package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +12 -3
  75. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +14 -4
  76. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +216 -0
  77. package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +142 -40
  78. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +14 -12
  79. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +6 -1
  80. package/src/flow/models/blocks/filter-manager/__tests__/getDefaultOperator.test.ts +35 -1
  81. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +148 -0
  82. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/defaultOperator.test.tsx +85 -0
  83. package/src/flow/models/blocks/filter-manager/flow-actions/connectFields.tsx +48 -6
  84. package/src/flow/models/blocks/filter-manager/flow-actions/customizeFilterRender.tsx +14 -44
  85. package/src/flow/models/blocks/filter-manager/flow-actions/defaultOperator.tsx +3 -5
  86. package/src/flow/models/blocks/filter-manager/flow-actions/operatorComponentProps.tsx +2 -12
  87. package/src/flow/models/blocks/filter-manager/utils.ts +143 -4
  88. package/src/flow/models/blocks/form/FormAssociationItemModel.tsx +9 -0
  89. package/src/flow/models/blocks/form/FormItemModel.tsx +9 -0
  90. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  91. package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +27 -2
  92. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +29 -0
  93. package/src/flow/models/blocks/form/value-runtime/runtime.ts +5 -1
  94. package/src/flow/models/blocks/table/TableColumnModel.tsx +15 -5
  95. package/src/flow/models/blocks/table/TableCustomColumnModel.tsx +2 -2
  96. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +18 -0
  97. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +6 -7
  98. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +5 -3
  99. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +70 -0
@@ -13,6 +13,10 @@ import type { FilterOperator, FilterOption } from '../../../flow/components/filt
13
13
  import { PasswordInput } from '../PasswordInput';
14
14
  import { DateFilterDynamicComponent } from './DateFilterDynamicComponent';
15
15
 
16
+ type ComponentRegistryApp = {
17
+ getComponent?: (name: string) => React.ComponentType<any> | undefined;
18
+ };
19
+
16
20
  export interface FilterValueInputProps {
17
21
  /** The currently selected leaf field option from the field picker. */
18
22
  field?: FilterOption;
@@ -26,17 +30,20 @@ export interface FilterValueInputProps {
26
30
  t?: (key: string) => string;
27
31
  /** Optional placeholder for the fallback `Input`. */
28
32
  placeholder?: string;
33
+ /** Optional v2 app registry used to resolve plugin-provided operator components. */
34
+ app?: ComponentRegistryApp;
29
35
  }
30
36
 
31
37
  const identity = (s: string) => s;
32
38
 
33
39
  type EffectiveSchema = {
34
- 'x-component'?: string;
40
+ 'x-component'?: string | React.ComponentType<any>;
35
41
  'x-component-props'?: Record<string, any>;
36
42
  enum?: Array<{ value: any; label: string }> | any[];
37
43
  };
38
44
 
39
45
  const baseStyle = { minWidth: 200 } as const;
46
+ const fallbackInputComponents = new Set(['Input', 'Input.URL', 'NanoIDInput']);
40
47
 
41
48
  /** Resolve operator-level schema → field uiSchema → fallback Input. */
42
49
  const resolveSchema = (field?: FilterOption, operator?: FilterOperator): EffectiveSchema => {
@@ -54,7 +61,7 @@ const resolveSchema = (field?: FilterOption, operator?: FilterOperator): Effecti
54
61
  * Interface-aware value renderer for filter rows. Returns `null` for `noValue` operators (`$empty`, `$notEmpty`). Otherwise dispatches the effective `x-component` (operator schema > field uiSchema > Input) to a small registry of antd controls.
55
62
  */
56
63
  export const FilterValueInput: React.FC<FilterValueInputProps> = (props) => {
57
- const { field, operator, value, onChange, t = identity, placeholder } = props;
64
+ const { field, operator, value, onChange, t = identity, placeholder, app } = props;
58
65
 
59
66
  if (operator?.noValue) {
60
67
  return null;
@@ -64,6 +71,12 @@ export const FilterValueInput: React.FC<FilterValueInputProps> = (props) => {
64
71
  const componentName = schema['x-component'];
65
72
  const componentProps = schema['x-component-props'] || {};
66
73
  const enumOptions = (schema as any).enum || (field?.schema as any)?.enum;
74
+ const customComponentProps = {
75
+ value,
76
+ onChange,
77
+ ...componentProps,
78
+ style: { ...baseStyle, ...(componentProps.style || {}) },
79
+ };
67
80
 
68
81
  switch (componentName) {
69
82
  case 'DateFilterDynamicComponent':
@@ -183,6 +196,18 @@ export const FilterValueInput: React.FC<FilterValueInputProps> = (props) => {
183
196
  case 'Input.URL':
184
197
  case 'NanoIDInput':
185
198
  default:
199
+ if (typeof componentName === 'function') {
200
+ const Component = componentName;
201
+ return <Component {...customComponentProps} />;
202
+ }
203
+
204
+ if (typeof componentName === 'string' && !fallbackInputComponents.has(componentName)) {
205
+ const Component = app?.getComponent?.(componentName);
206
+ if (Component) {
207
+ return <Component {...customComponentProps} />;
208
+ }
209
+ }
210
+
186
211
  return (
187
212
  <Input
188
213
  value={value}
@@ -222,6 +222,45 @@ describe('FilterValueInput schema precedence', () => {
222
222
  expect(document.querySelector('.ant-select')).not.toBeNull();
223
223
  });
224
224
 
225
+ it('operator schema can render an app-registered custom component', () => {
226
+ const onChange = vi.fn();
227
+ const CustomFilterInput = (props: any) => (
228
+ <button
229
+ type="button"
230
+ data-testid="custom-filter-input"
231
+ data-field-interface={props.fieldInterface}
232
+ data-value={JSON.stringify(props.value)}
233
+ onClick={() => props.onChange?.(['alpha', 'beta'])}
234
+ >
235
+ custom-filter-input
236
+ </button>
237
+ );
238
+
239
+ render(
240
+ <FilterValueInput
241
+ operator={opOf({
242
+ value: '$in',
243
+ schema: {
244
+ 'x-component': 'MultipleKeywordsInput',
245
+ 'x-component-props': { fieldInterface: 'input' },
246
+ },
247
+ })}
248
+ value={['foo', 'bar']}
249
+ onChange={onChange}
250
+ app={{
251
+ getComponent: (name) => (name === 'MultipleKeywordsInput' ? CustomFilterInput : undefined),
252
+ }}
253
+ />,
254
+ );
255
+
256
+ const input = screen.getByTestId('custom-filter-input');
257
+ expect(input.getAttribute('data-field-interface')).toBe('input');
258
+ expect(input.getAttribute('data-value')).toBe(JSON.stringify(['foo', 'bar']));
259
+
260
+ fireEvent.click(input);
261
+ expect(onChange).toHaveBeenCalledWith(['alpha', 'beta']);
262
+ });
263
+
225
264
  it('field uiSchema is used when operator has none', () => {
226
265
  const { container } = render(
227
266
  <FilterValueInput
@@ -16,4 +16,5 @@ export * from './FileSizeInput';
16
16
  export * from './JsonTextArea';
17
17
  export * from './PasswordInput';
18
18
  export * from './RemoteSelect';
19
+ export * from './TypedVariableInput';
19
20
  export * from './VariableInput';
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import React from 'react';
11
- import { describe, it, expect, vi, beforeEach } from 'vitest';
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
12
  import { MemoryRouter, Route, Routes } from 'react-router-dom';
13
13
  import { render, screen, waitFor } from '@testing-library/react';
14
14
  import { FlowContextProvider, FlowEngine, FlowEngineProvider, type FlowModel } from '@nocobase/flow-engine';
@@ -32,6 +32,13 @@ describe('FlowRoute', () => {
32
32
  beforeEach(() => {
33
33
  vi.clearAllMocks();
34
34
  hookState.refresh = vi.fn();
35
+ // Fixtures mount the modern client under the `v2` segment; tell the
36
+ // runtime-prefix helper so v2-runtime detection matches.
37
+ (globalThis.window as any).__nocobase_modern_client_prefix__ = 'v2';
38
+ });
39
+
40
+ afterEach(() => {
41
+ delete (globalThis.window as any).__nocobase_modern_client_prefix__;
35
42
  });
36
43
 
37
44
  it('should bridge page lifecycle to admin-layout-model', async () => {
@@ -0,0 +1,100 @@
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 { describe, expect, it, vi } from 'vitest';
11
+ import { actionLinkageRules } from '../linkageRules';
12
+
13
+ function createRule(actions: any[]) {
14
+ return {
15
+ key: 'r1',
16
+ title: 'r1',
17
+ enable: true,
18
+ condition: { logic: '$and', items: [] },
19
+ actions,
20
+ };
21
+ }
22
+
23
+ describe('linkageRules hidden state propagation', () => {
24
+ it('restores hidden state to same-target models when reverting to original props', async () => {
25
+ const blockModel = { uid: 'block-1' };
26
+
27
+ const hostModel: any = {
28
+ uid: 'host',
29
+ hidden: false,
30
+ context: { blockModel },
31
+ __allModels: [],
32
+ setProps: vi.fn(),
33
+ getFlow: vi.fn(() => ({})),
34
+ getStepParams: vi.fn((_flowKey: string, stepKey: string) => {
35
+ if (stepKey === 'init') return { fieldPath: 'org_m2o' };
36
+ return undefined;
37
+ }),
38
+ translate: (s: string) => s,
39
+ };
40
+
41
+ const sameTargetModel: any = {
42
+ uid: 'same-target',
43
+ hidden: false,
44
+ context: { blockModel },
45
+ setProps: vi.fn(),
46
+ getStepParams: vi.fn((_flowKey: string, stepKey: string) => {
47
+ if (stepKey === 'init') return { fieldPath: 'org_m2o' };
48
+ return undefined;
49
+ }),
50
+ translate: (s: string) => s,
51
+ };
52
+
53
+ const engine = {
54
+ forEachModel: (visitor: (m: any) => void) => {
55
+ [hostModel, sameTargetModel].forEach(visitor);
56
+ },
57
+ };
58
+
59
+ const ctx: any = {
60
+ flowKey: 'buttonSettings',
61
+ model: hostModel,
62
+ engine,
63
+ app: {
64
+ jsonLogic: {
65
+ apply: vi.fn(() => true),
66
+ },
67
+ },
68
+ resolveJsonTemplate: vi.fn(async (value: any) => value),
69
+ getAction: (name: string) => {
70
+ if (name !== 'linkageSetActionProps') return undefined;
71
+ return {
72
+ handler: async (_ctx: any, params: any) => {
73
+ params.setProps(hostModel, {
74
+ hiddenModel: params.value === 'hidden',
75
+ disabled: false,
76
+ hiddenText: false,
77
+ });
78
+ },
79
+ };
80
+ },
81
+ };
82
+
83
+ await actionLinkageRules.handler(ctx, {
84
+ value: [
85
+ createRule([
86
+ {
87
+ name: 'linkageSetActionProps',
88
+ params: { value: 'hidden' },
89
+ },
90
+ ]),
91
+ ],
92
+ });
93
+ expect(hostModel.hidden).toBe(true);
94
+ expect(sameTargetModel.hidden).toBe(true);
95
+
96
+ await actionLinkageRules.handler(ctx, { value: [] });
97
+ expect(hostModel.hidden).toBe(false);
98
+ expect(sameTargetModel.hidden).toBe(false);
99
+ });
100
+ });
@@ -9,9 +9,15 @@
9
9
 
10
10
  import React from 'react';
11
11
  import { fireEvent, render, screen } from '@testing-library/react';
12
- import { FlowSettingsContextProvider } from '@nocobase/flow-engine';
12
+ import { FlowEngine, FlowModel, FlowSettingsContextProvider } from '@nocobase/flow-engine';
13
13
  import { describe, expect, it, vi } from 'vitest';
14
- import { fieldLinkageRules, linkageSetFieldProps, subFormLinkageSetFieldProps } from '../linkageRules';
14
+ import {
15
+ fieldLinkageRules,
16
+ linkageAssignField,
17
+ linkageSetFieldProps,
18
+ subFormFieldLinkageRules,
19
+ subFormLinkageSetFieldProps,
20
+ } from '../linkageRules';
15
21
 
16
22
  const createSubFormFieldModel = ({
17
23
  uid,
@@ -335,6 +341,110 @@ describe('subFormLinkageSetFieldProps action', () => {
335
341
  expect(row0CollectedModel.hidden).toBe(false);
336
342
  expect(row1CollectedModel.hidden).toBe(true);
337
343
  });
344
+
345
+ it('should clear the current row value when a subform list field is hidden without reserving value', async () => {
346
+ const setFormValues = vi.fn(async () => undefined);
347
+ const form = {
348
+ getFieldValue: vi.fn((path: Array<string | number>) => {
349
+ if (JSON.stringify(path) === JSON.stringify(['items', 0, 'a'])) {
350
+ return 'row value';
351
+ }
352
+ return undefined;
353
+ }),
354
+ setFieldValue: vi.fn(),
355
+ };
356
+ const engine = new FlowEngine();
357
+
358
+ const rowGridFork = new FlowModel({ uid: 'row-grid-fork', flowEngine: engine }) as any;
359
+ rowGridFork.hidden = false;
360
+ rowGridFork.context.defineProperty('fieldKey', { value: ['items:0'] });
361
+ rowGridFork.context.defineProperty('fieldIndex', { value: ['items:0'] });
362
+ rowGridFork.context.defineProperty('form', { value: form });
363
+ rowGridFork.context.defineProperty('setFormValues', { value: setFormValues });
364
+ rowGridFork.context.defineProperty('app', {
365
+ value: {
366
+ jsonLogic: {
367
+ apply: () => true,
368
+ },
369
+ },
370
+ });
371
+ rowGridFork.getAction = vi.fn((name: string) => {
372
+ if (name === 'subFormLinkageSetFieldProps') {
373
+ return subFormLinkageSetFieldProps;
374
+ }
375
+ });
376
+
377
+ const targetFieldFork: any = {
378
+ uid: 'field-a',
379
+ isFork: true,
380
+ hidden: false,
381
+ props: {},
382
+ context: {
383
+ fieldIndex: ['items:0'],
384
+ },
385
+ getStepParams: vi.fn((flowKey: string, stepKey: string) => {
386
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
387
+ return { fieldPath: 'a' };
388
+ }
389
+ }),
390
+ setProps(key: any, value?: any) {
391
+ if (typeof key === 'string') {
392
+ this.props[key] = value;
393
+ } else {
394
+ this.props = { ...this.props, ...key };
395
+ }
396
+ },
397
+ };
398
+
399
+ const formItemModel: any = {
400
+ uid: 'field-a',
401
+ getFork: vi.fn((key: string) => (key === 'items:0:field-a' ? targetFieldFork : undefined)),
402
+ };
403
+
404
+ engine.getModel = vi.fn((uid: string) => (uid === 'field-a' ? formItemModel : undefined)) as any;
405
+
406
+ await subFormFieldLinkageRules.handler(
407
+ {
408
+ model: {
409
+ hidden: false,
410
+ subModels: {
411
+ grid: {
412
+ forks: [rowGridFork],
413
+ },
414
+ },
415
+ },
416
+ flowKey: 'eventSettings',
417
+ } as any,
418
+ {
419
+ value: [
420
+ {
421
+ key: 'rule-1',
422
+ enable: true,
423
+ condition: { logic: '$and', items: [] },
424
+ actions: [
425
+ {
426
+ name: 'subFormLinkageSetFieldProps',
427
+ params: {
428
+ value: {
429
+ fields: ['field-a'],
430
+ state: 'hidden',
431
+ },
432
+ },
433
+ },
434
+ ],
435
+ },
436
+ ],
437
+ },
438
+ );
439
+
440
+ expect(form.getFieldValue).toHaveBeenCalledWith(['items', 0, 'a']);
441
+ expect(targetFieldFork.hidden).toBe(true);
442
+ expect(setFormValues).toHaveBeenCalledWith(
443
+ [{ path: ['items', 0, 'a'], value: undefined }],
444
+ expect.objectContaining({ source: 'linkage' }),
445
+ );
446
+ expect(form.setFieldValue).not.toHaveBeenCalled();
447
+ });
338
448
  });
339
449
 
340
450
  describe('linkageSetFieldProps action', () => {
@@ -541,6 +651,263 @@ describe('linkageSetFieldProps action', () => {
541
651
  expect(form.setFieldValue).not.toHaveBeenCalled();
542
652
  });
543
653
 
654
+ it('should clear form value when a field is hidden without reserving value', async () => {
655
+ const setFormValues = vi.fn(async () => undefined);
656
+ const form = {
657
+ getFieldValue: vi.fn(() => '123'),
658
+ setFieldValue: vi.fn(),
659
+ };
660
+ const fieldModel: any = {
661
+ uid: 'name-field',
662
+ hidden: false,
663
+ context: { form },
664
+ props: {
665
+ label: 'Name',
666
+ },
667
+ getStepParams: vi.fn((flowKey: string, stepKey: string) => {
668
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
669
+ return { fieldPath: 'name' };
670
+ }
671
+ }),
672
+ setProps(key: any, value?: any) {
673
+ if (typeof key === 'string') {
674
+ this.props[key] = value;
675
+ } else {
676
+ this.props = { ...this.props, ...key };
677
+ }
678
+ },
679
+ };
680
+ const ctx: any = {
681
+ app: {
682
+ jsonLogic: {
683
+ apply: vi.fn(() => true),
684
+ },
685
+ },
686
+ model: {
687
+ context: { form },
688
+ subModels: {
689
+ grid: {
690
+ subModels: {
691
+ items: [fieldModel],
692
+ },
693
+ },
694
+ },
695
+ },
696
+ setFormValues,
697
+ getAction: (name: string) => (name === 'linkageSetFieldProps' ? linkageSetFieldProps : null),
698
+ resolveJsonTemplate: vi.fn(async (value) => value),
699
+ };
700
+
701
+ await fieldLinkageRules.handler(ctx, {
702
+ value: [
703
+ {
704
+ key: 'rule-1',
705
+ enable: true,
706
+ condition: { logic: '$and', items: [] },
707
+ actions: [
708
+ {
709
+ key: 'action-1',
710
+ name: 'linkageSetFieldProps',
711
+ params: {
712
+ value: {
713
+ fields: ['name-field'],
714
+ state: 'hidden',
715
+ },
716
+ },
717
+ },
718
+ ],
719
+ },
720
+ ],
721
+ });
722
+
723
+ expect(fieldModel.hidden).toBe(true);
724
+ expect(setFormValues).toHaveBeenCalledWith(
725
+ [{ path: ['name'], value: undefined }],
726
+ expect.objectContaining({ source: 'linkage' }),
727
+ );
728
+ expect(form.setFieldValue).not.toHaveBeenCalled();
729
+ });
730
+
731
+ it('should keep hidden clear after same-round assignment for the same field', async () => {
732
+ const formValues: { name?: string } = {};
733
+ const setFormValues = vi.fn(async (patches: Array<{ path: Array<string | number>; value: string | undefined }>) => {
734
+ for (const patch of patches) {
735
+ if (patch.path.length === 1 && patch.path[0] === 'name') {
736
+ formValues.name = patch.value;
737
+ }
738
+ }
739
+ });
740
+ const form = {
741
+ getFieldValue: vi.fn((path: Array<string | number>) => {
742
+ if (path.length === 1 && path[0] === 'name') {
743
+ return formValues.name;
744
+ }
745
+ return undefined;
746
+ }),
747
+ setFieldValue: vi.fn(),
748
+ };
749
+ const fieldModel: any = {
750
+ uid: 'name-field',
751
+ hidden: false,
752
+ context: { form },
753
+ props: {
754
+ label: 'Name',
755
+ },
756
+ getStepParams: vi.fn((flowKey: string, stepKey: string) => {
757
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
758
+ return { fieldPath: 'name' };
759
+ }
760
+ }),
761
+ setProps(key: any, value?: any) {
762
+ if (typeof key === 'string') {
763
+ this.props[key] = value;
764
+ } else {
765
+ this.props = { ...this.props, ...key };
766
+ }
767
+ },
768
+ };
769
+ const ctx: any = {
770
+ app: {
771
+ jsonLogic: {
772
+ apply: vi.fn(() => true),
773
+ },
774
+ },
775
+ model: {
776
+ context: { form },
777
+ subModels: {
778
+ grid: {
779
+ subModels: {
780
+ items: [fieldModel],
781
+ },
782
+ },
783
+ },
784
+ },
785
+ setFormValues,
786
+ getAction: (name: string) => {
787
+ if (name === 'linkageSetFieldProps') return linkageSetFieldProps;
788
+ if (name === 'linkageAssignField') return linkageAssignField;
789
+ return null;
790
+ },
791
+ resolveJsonTemplate: vi.fn(async (value) => value),
792
+ };
793
+
794
+ await fieldLinkageRules.handler(ctx, {
795
+ value: [
796
+ {
797
+ key: 'rule-1',
798
+ enable: true,
799
+ condition: { logic: '$and', items: [] },
800
+ actions: [
801
+ {
802
+ key: 'action-1',
803
+ name: 'linkageSetFieldProps',
804
+ params: {
805
+ value: {
806
+ fields: ['name-field'],
807
+ state: 'hidden',
808
+ },
809
+ },
810
+ },
811
+ {
812
+ key: 'action-2',
813
+ name: 'linkageAssignField',
814
+ params: {
815
+ value: [
816
+ {
817
+ key: 'assign-1',
818
+ enable: true,
819
+ targetPath: 'name',
820
+ value: 'assigned',
821
+ },
822
+ ],
823
+ },
824
+ },
825
+ ],
826
+ },
827
+ ],
828
+ });
829
+
830
+ expect(fieldModel.hidden).toBe(true);
831
+ expect(formValues.name).toBeUndefined();
832
+ expect(setFormValues).not.toHaveBeenCalled();
833
+ expect(form.setFieldValue).not.toHaveBeenCalled();
834
+ });
835
+
836
+ it('should not clear form value when a field is hidden with reserved value', async () => {
837
+ const setFormValues = vi.fn(async () => undefined);
838
+ const form = {
839
+ getFieldValue: vi.fn(() => '123'),
840
+ setFieldValue: vi.fn(),
841
+ };
842
+ const fieldModel: any = {
843
+ uid: 'name-field',
844
+ hidden: false,
845
+ context: { form },
846
+ props: {
847
+ label: 'Name',
848
+ },
849
+ getStepParams: vi.fn((flowKey: string, stepKey: string) => {
850
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
851
+ return { fieldPath: 'name' };
852
+ }
853
+ }),
854
+ setProps(key: any, value?: any) {
855
+ if (typeof key === 'string') {
856
+ this.props[key] = value;
857
+ } else {
858
+ this.props = { ...this.props, ...key };
859
+ }
860
+ },
861
+ };
862
+ const ctx: any = {
863
+ app: {
864
+ jsonLogic: {
865
+ apply: vi.fn(() => true),
866
+ },
867
+ },
868
+ model: {
869
+ context: { form },
870
+ subModels: {
871
+ grid: {
872
+ subModels: {
873
+ items: [fieldModel],
874
+ },
875
+ },
876
+ },
877
+ },
878
+ setFormValues,
879
+ getAction: (name: string) => (name === 'linkageSetFieldProps' ? linkageSetFieldProps : null),
880
+ resolveJsonTemplate: vi.fn(async (value) => value),
881
+ };
882
+
883
+ await fieldLinkageRules.handler(ctx, {
884
+ value: [
885
+ {
886
+ key: 'rule-1',
887
+ enable: true,
888
+ condition: { logic: '$and', items: [] },
889
+ actions: [
890
+ {
891
+ key: 'action-1',
892
+ name: 'linkageSetFieldProps',
893
+ params: {
894
+ value: {
895
+ fields: ['name-field'],
896
+ state: 'hiddenReservedValue',
897
+ },
898
+ },
899
+ },
900
+ ],
901
+ },
902
+ ],
903
+ });
904
+
905
+ expect(fieldModel.hidden).toBe(false);
906
+ expect(fieldModel.props.hidden).toBe(true);
907
+ expect(setFormValues).not.toHaveBeenCalled();
908
+ expect(form.setFieldValue).not.toHaveBeenCalled();
909
+ });
910
+
544
911
  it('should keep all options selectable after options were limited once', async () => {
545
912
  const fieldComponentModel: any = {
546
913
  uid: 'status-field-component',
@@ -46,7 +46,7 @@ describe('linkageRulesRefresh action', () => {
46
46
  expect(handler).not.toHaveBeenCalled();
47
47
  });
48
48
 
49
- it('runs linkage action on master model when forks are not mounted in runtime mode', async () => {
49
+ it('skips master model when forks can handle flow even if regular action buttons do not attach refs', async () => {
50
50
  const handler = vi.fn(async () => {});
51
51
  const model: any = {
52
52
  isFork: false,
@@ -74,6 +74,28 @@ describe('linkageRulesRefresh action', () => {
74
74
  flowKey: 'buttonSettings',
75
75
  });
76
76
 
77
+ expect(handler).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it('runs linkage action on master model when no forks exist in runtime mode', async () => {
81
+ const handler = vi.fn(async () => {});
82
+ const model: any = {
83
+ isFork: false,
84
+ forks: new Set(),
85
+ getFlow: vi.fn(() => ({})),
86
+ getStepParams: vi.fn(() => ({ value: ['master-runtime'] })),
87
+ };
88
+ const ctx: any = {
89
+ model,
90
+ resolveJsonTemplate: vi.fn(async (p: any) => p),
91
+ getAction: vi.fn(() => ({ handler })),
92
+ };
93
+
94
+ await linkageRulesRefresh.handler(ctx, {
95
+ actionName: 'actionLinkageRules',
96
+ flowKey: 'buttonSettings',
97
+ });
98
+
77
99
  expect(handler).toHaveBeenCalledWith(ctx, { value: ['master-runtime'] });
78
100
  });
79
101
 
@@ -33,7 +33,14 @@ export const dataScope = defineAction({
33
33
  return (
34
34
  <FilterGroup
35
35
  value={props.value}
36
- FilterItem={(p) => <VariableFilterItem {...p} model={flowContext.model} rightAsVariable />}
36
+ FilterItem={(p) => (
37
+ <VariableFilterItem
38
+ {...p}
39
+ model={flowContext.model}
40
+ rightAsVariable
41
+ maxAssociationFieldDepth={flowContext.model instanceof FieldModel ? undefined : 2}
42
+ />
43
+ )}
37
44
  />
38
45
  );
39
46
  },