@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.
- package/es/authRedirect.d.ts +12 -0
- package/es/collection-manager/filter-operators/index.d.ts +1 -0
- package/es/components/form/TypedVariableInput.d.ts +75 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +5 -1
- package/es/components/form/filter/FilterValueInput.d.ts +5 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/components/ConditionBuilder.d.ts +1 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/components/FieldAssignValueInput.d.ts +1 -0
- package/es/flow/components/fieldAssignOptions.d.ts +10 -0
- package/es/flow/components/filter/LinkageFilterItem.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +1 -0
- package/es/flow/components/filter/metaTreeAssociationDepth.d.ts +13 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +26 -3
- package/es/flow/models/base/AssociationFieldGroupModel.d.ts +2 -33
- package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +8 -0
- package/es/flow/models/blocks/filter-manager/utils.d.ts +23 -1
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +2 -0
- package/es/index.mjs +106 -106
- package/lib/index.js +106 -106
- package/package.json +7 -7
- package/src/__tests__/browserChecker.test.ts +103 -0
- package/src/__tests__/dataSourceRuntime.test.tsx +22 -1
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +5 -0
- package/src/authRedirect.ts +43 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +14 -7
- package/src/collection-manager/filter-operators/index.ts +29 -2
- package/src/components/README.md +48 -0
- package/src/components/README.zh-CN.md +48 -0
- package/src/components/form/TypedVariableInput.tsx +441 -0
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +152 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +10 -2
- package/src/components/form/filter/FilterValueInput.tsx +27 -2
- package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +39 -0
- package/src/components/form/index.tsx +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +8 -1
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +100 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +369 -2
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +23 -1
- package/src/flow/actions/dataScope.tsx +8 -1
- package/src/flow/actions/linkageRules.tsx +61 -5
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -3
- package/src/flow/actions/setTargetDataScope.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +15 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +75 -1
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +11 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -4
- package/src/flow/common/Markdown/Edit.tsx +32 -4
- package/src/flow/components/ConditionBuilder.tsx +12 -2
- package/src/flow/components/DefaultValue.tsx +14 -8
- package/src/flow/components/FieldAssignRulesEditor.tsx +37 -46
- package/src/flow/components/FieldAssignValueInput.tsx +14 -14
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +112 -1
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +71 -1
- package/src/flow/components/code-editor/__tests__/runjsCompletions.test.ts +272 -27
- package/src/flow/components/code-editor/runjsCompletions.ts +490 -9
- package/src/flow/components/fieldAssignOptions.ts +23 -6
- package/src/flow/components/filter/LinkageFilterItem.tsx +26 -12
- package/src/flow/components/filter/VariableFilterItem.tsx +32 -13
- package/src/flow/components/filter/__tests__/fieldsToOptions.test.ts +48 -0
- package/src/flow/components/filter/__tests__/metaTreeAssociationDepth.test.ts +60 -0
- package/src/flow/components/filter/fieldsToOptions.ts +9 -4
- package/src/flow/components/filter/metaTreeAssociationDepth.ts +69 -0
- package/src/flow/internal/utils/operatorSchemaHelper.ts +112 -5
- package/src/flow/models/actions/FilterActionModel.tsx +15 -3
- package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +90 -0
- package/src/flow/models/base/AssociationFieldGroupModel.tsx +20 -3
- package/src/flow/models/base/GridModel.tsx +1 -1
- package/src/flow/models/base/PageModel/ChildPageModel.tsx +5 -1
- package/src/flow/models/base/PageModel/__tests__/ChildPageModel.test.ts +12 -0
- package/src/flow/models/base/__tests__/AssociationFieldGroupModel.test.ts +86 -0
- package/src/flow/models/base/__tests__/GridModel.render.test.tsx +53 -0
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +12 -3
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +14 -4
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +216 -0
- package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +142 -40
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +14 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +6 -1
- package/src/flow/models/blocks/filter-manager/__tests__/getDefaultOperator.test.ts +35 -1
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +148 -0
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/defaultOperator.test.tsx +85 -0
- package/src/flow/models/blocks/filter-manager/flow-actions/connectFields.tsx +48 -6
- package/src/flow/models/blocks/filter-manager/flow-actions/customizeFilterRender.tsx +14 -44
- package/src/flow/models/blocks/filter-manager/flow-actions/defaultOperator.tsx +3 -5
- package/src/flow/models/blocks/filter-manager/flow-actions/operatorComponentProps.tsx +2 -12
- package/src/flow/models/blocks/filter-manager/utils.ts +143 -4
- package/src/flow/models/blocks/form/FormAssociationItemModel.tsx +9 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +9 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +27 -2
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +29 -0
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +5 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +15 -5
- package/src/flow/models/blocks/table/TableCustomColumnModel.tsx +2 -2
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +18 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +6 -7
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +5 -3
- 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
|
|
@@ -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 {
|
|
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('
|
|
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) =>
|
|
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
|
},
|