@nocobase/client-v2 2.1.0-beta.27 → 2.1.0-beta.29
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/components/form/JsonTextArea.d.ts +18 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/dateRangeLimit.d.ts +9 -0
- package/es/flow/actions/index.d.ts +1 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +4 -0
- package/es/flow/models/base/PageModel/RootPageModel.d.ts +9 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +7 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +2 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +20 -0
- package/es/flow/models/fields/JSEditableFieldModel.d.ts +4 -0
- package/es/index.mjs +72 -65
- package/lib/index.js +61 -54
- package/package.json +6 -5
- package/src/components/form/JsonTextArea.tsx +129 -0
- package/src/components/index.ts +1 -0
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +478 -0
- package/src/flow/actions/__tests__/pattern.test.ts +190 -0
- package/src/flow/actions/dateRangeLimit.tsx +66 -0
- package/src/flow/actions/index.ts +1 -0
- package/src/flow/actions/linkageRules.tsx +117 -19
- package/src/flow/actions/openView.tsx +2 -1
- package/src/flow/actions/pattern.tsx +25 -2
- package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +7 -1
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +117 -0
- package/src/flow/models/base/PageModel/PageModel.tsx +15 -3
- package/src/flow/models/base/PageModel/RootPageModel.tsx +37 -2
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +73 -0
- package/src/flow/models/base/PageModel/__tests__/RootPageModel.test.ts +116 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +167 -1
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +103 -11
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +27 -3
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +47 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +42 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.refresh.test.tsx +122 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +2 -0
- package/src/flow/models/fields/ClickableFieldModel.tsx +21 -9
- package/src/flow/models/fields/DateTimeFieldModel/DateOnlyFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeFieldModel.tsx +4 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeNoTzFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeTzFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +242 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +152 -0
- package/src/flow/models/fields/JSEditableFieldModel.tsx +110 -14
- package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +87 -0
- package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +210 -0
|
@@ -0,0 +1,190 @@
|
|
|
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 { FlowEngine, FlowModel } from '@nocobase/flow-engine';
|
|
12
|
+
import { FieldModel } from '../../models/base/FieldModel';
|
|
13
|
+
import { JSEditableFieldModel } from '../../models/fields/JSEditableFieldModel';
|
|
14
|
+
import { DetailsItemModel } from '../../models/blocks/details/DetailsItemModel';
|
|
15
|
+
import { pattern } from '../pattern';
|
|
16
|
+
|
|
17
|
+
class DummyDisplayFieldModel extends FieldModel {}
|
|
18
|
+
|
|
19
|
+
class DummyFormItemModel extends FlowModel<{ subModels: { field?: FieldModel } }> {
|
|
20
|
+
collectionField: any;
|
|
21
|
+
|
|
22
|
+
static getDefaultBindingByField() {
|
|
23
|
+
return { modelName: 'FieldModel' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getFieldSettingsInitParams() {
|
|
27
|
+
return { mock: true };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeCollectionField() {
|
|
32
|
+
return {
|
|
33
|
+
targetCollection: undefined,
|
|
34
|
+
isAssociationField: () => false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeCtx(parent: DummyFormItemModel) {
|
|
39
|
+
const collectionField = makeCollectionField();
|
|
40
|
+
parent.collectionField = collectionField;
|
|
41
|
+
return {
|
|
42
|
+
model: parent,
|
|
43
|
+
collectionField,
|
|
44
|
+
engine: parent.flowEngine,
|
|
45
|
+
} as any;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('pattern action', () => {
|
|
49
|
+
it('keeps JS editable field model when switching to display only', async () => {
|
|
50
|
+
const engine = new FlowEngine();
|
|
51
|
+
engine.registerModels({
|
|
52
|
+
DummyFormItemModel,
|
|
53
|
+
FieldModel,
|
|
54
|
+
JSEditableFieldModel,
|
|
55
|
+
DummyDisplayFieldModel,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const parent = engine.createModel<DummyFormItemModel>({
|
|
59
|
+
use: DummyFormItemModel,
|
|
60
|
+
uid: 'form-item-js',
|
|
61
|
+
subModels: {
|
|
62
|
+
field: {
|
|
63
|
+
use: FieldModel,
|
|
64
|
+
uid: 'field-js',
|
|
65
|
+
stepParams: {
|
|
66
|
+
fieldBinding: {
|
|
67
|
+
use: 'JSEditableFieldModel',
|
|
68
|
+
},
|
|
69
|
+
jsSettings: {
|
|
70
|
+
runJs: {
|
|
71
|
+
code: 'ctx.render("hello")',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const field = parent.subModels.field;
|
|
79
|
+
const saveSpy = vi.spyOn(engine, 'saveModel');
|
|
80
|
+
const applyJsSettingsSpy = vi.spyOn(field as JSEditableFieldModel, 'scheduleApplyJsSettings');
|
|
81
|
+
|
|
82
|
+
await pattern.afterParamsSave?.(makeCtx(parent), { pattern: 'readPretty' }, { pattern: 'editable' });
|
|
83
|
+
|
|
84
|
+
expect(parent.subModels.field).toBe(field);
|
|
85
|
+
expect(parent.subModels.field).toBeInstanceOf(JSEditableFieldModel);
|
|
86
|
+
expect(parent.subModels.field?.uid).toBe('field-js');
|
|
87
|
+
expect(parent.subModels.field?.getStepParams('jsSettings', 'runJs')).toMatchObject({
|
|
88
|
+
code: 'ctx.render("hello")',
|
|
89
|
+
});
|
|
90
|
+
expect(saveSpy).not.toHaveBeenCalled();
|
|
91
|
+
expect(applyJsSettingsSpy).toHaveBeenCalledTimes(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('keeps JS editable field model when leaving display only', async () => {
|
|
95
|
+
const engine = new FlowEngine();
|
|
96
|
+
engine.registerModels({
|
|
97
|
+
DummyFormItemModel,
|
|
98
|
+
FieldModel,
|
|
99
|
+
JSEditableFieldModel,
|
|
100
|
+
DummyDisplayFieldModel,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const parent = engine.createModel<DummyFormItemModel>({
|
|
104
|
+
use: DummyFormItemModel,
|
|
105
|
+
uid: 'form-item-js-leave',
|
|
106
|
+
subModels: {
|
|
107
|
+
field: {
|
|
108
|
+
use: FieldModel,
|
|
109
|
+
uid: 'field-js-leave',
|
|
110
|
+
stepParams: {
|
|
111
|
+
fieldBinding: {
|
|
112
|
+
use: 'JSEditableFieldModel',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
const field = parent.subModels.field;
|
|
119
|
+
const saveSpy = vi.spyOn(engine, 'saveModel');
|
|
120
|
+
const applyJsSettingsSpy = vi.spyOn(field as JSEditableFieldModel, 'scheduleApplyJsSettings');
|
|
121
|
+
|
|
122
|
+
await pattern.afterParamsSave?.(makeCtx(parent), { pattern: 'editable' }, { pattern: 'readPretty' });
|
|
123
|
+
|
|
124
|
+
expect(parent.subModels.field).toBe(field);
|
|
125
|
+
expect(parent.subModels.field).toBeInstanceOf(JSEditableFieldModel);
|
|
126
|
+
expect(saveSpy).not.toHaveBeenCalled();
|
|
127
|
+
expect(applyJsSettingsSpy).toHaveBeenCalledTimes(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('does not reapply JS settings when pattern is unchanged', async () => {
|
|
131
|
+
const engine = new FlowEngine();
|
|
132
|
+
engine.registerModels({
|
|
133
|
+
DummyFormItemModel,
|
|
134
|
+
FieldModel,
|
|
135
|
+
JSEditableFieldModel,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const parent = engine.createModel<DummyFormItemModel>({
|
|
139
|
+
use: DummyFormItemModel,
|
|
140
|
+
uid: 'form-item-js-unchanged',
|
|
141
|
+
subModels: {
|
|
142
|
+
field: {
|
|
143
|
+
use: FieldModel,
|
|
144
|
+
uid: 'field-js-unchanged',
|
|
145
|
+
stepParams: {
|
|
146
|
+
fieldBinding: {
|
|
147
|
+
use: 'JSEditableFieldModel',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
const applyJsSettingsSpy = vi.spyOn(parent.subModels.field as JSEditableFieldModel, 'scheduleApplyJsSettings');
|
|
154
|
+
|
|
155
|
+
await pattern.afterParamsSave?.(makeCtx(parent), { pattern: 'readPretty' }, { pattern: 'readPretty' });
|
|
156
|
+
|
|
157
|
+
expect(applyJsSettingsSpy).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('still rebuilds regular fields when switching to display only', async () => {
|
|
161
|
+
const engine = new FlowEngine();
|
|
162
|
+
engine.registerModels({
|
|
163
|
+
DummyFormItemModel,
|
|
164
|
+
FieldModel,
|
|
165
|
+
DummyDisplayFieldModel,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const parent = engine.createModel<DummyFormItemModel>({
|
|
169
|
+
use: DummyFormItemModel,
|
|
170
|
+
uid: 'form-item-regular',
|
|
171
|
+
subModels: {
|
|
172
|
+
field: {
|
|
173
|
+
use: FieldModel,
|
|
174
|
+
uid: 'field-regular',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
const ctx = makeCtx(parent);
|
|
179
|
+
const displayBinding = { modelName: 'DummyDisplayFieldModel', defaultProps: { display: true } } as any;
|
|
180
|
+
const getDisplayBindingSpy = vi.spyOn(DetailsItemModel, 'getDefaultBindingByField').mockReturnValue(displayBinding);
|
|
181
|
+
|
|
182
|
+
await pattern.afterParamsSave?.(ctx, { pattern: 'readPretty' }, { pattern: 'editable' });
|
|
183
|
+
|
|
184
|
+
expect(parent.subModels.field).toBeInstanceOf(DummyDisplayFieldModel);
|
|
185
|
+
expect(parent.subModels.field?.uid).toBe('field-regular');
|
|
186
|
+
expect(parent.subModels.field?.props).toMatchObject({ display: true });
|
|
187
|
+
|
|
188
|
+
getDisplayBindingSpy.mockRestore();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
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 { defineAction, tExpr } from '@nocobase/flow-engine';
|
|
11
|
+
import { FieldAssignValueInput } from '../components/FieldAssignValueInput';
|
|
12
|
+
|
|
13
|
+
function normalizeDateRangeValue(value: any) {
|
|
14
|
+
if (value === '' || value === null || typeof value === 'undefined') {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const dateRangeLimit = defineAction({
|
|
21
|
+
name: 'dateRangeLimit',
|
|
22
|
+
title: tExpr('Date range limit'),
|
|
23
|
+
uiMode: {
|
|
24
|
+
type: 'dialog',
|
|
25
|
+
props: {
|
|
26
|
+
width: 720,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
uiSchema(ctx: any) {
|
|
30
|
+
const targetPath = ctx.model.context?.collectionField?.name || '';
|
|
31
|
+
const dateLimitInputProps = {
|
|
32
|
+
targetPath,
|
|
33
|
+
enableDateVariableAsConstant: true,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
_minDate: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
title: tExpr('MinDate'),
|
|
40
|
+
'x-decorator': 'FormItem',
|
|
41
|
+
'x-component': FieldAssignValueInput,
|
|
42
|
+
'x-component-props': dateLimitInputProps,
|
|
43
|
+
},
|
|
44
|
+
_maxDate: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
title: tExpr('MaxDate'),
|
|
47
|
+
'x-decorator': 'FormItem',
|
|
48
|
+
'x-component': FieldAssignValueInput,
|
|
49
|
+
'x-component-props': dateLimitInputProps,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
defaultParams(ctx: any) {
|
|
54
|
+
return {
|
|
55
|
+
_minDate: normalizeDateRangeValue(ctx.model.props?._minDate),
|
|
56
|
+
_maxDate: normalizeDateRangeValue(ctx.model.props?._maxDate),
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
useRawParams: true,
|
|
60
|
+
async handler(ctx: any, params) {
|
|
61
|
+
ctx.model.setProps({
|
|
62
|
+
_minDate: normalizeDateRangeValue(params?._minDate),
|
|
63
|
+
_maxDate: normalizeDateRangeValue(params?._maxDate),
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -20,6 +20,7 @@ export * from './refreshTargetBlocks';
|
|
|
20
20
|
export * from './setTargetDataScope';
|
|
21
21
|
export { titleField } from './titleField';
|
|
22
22
|
export * from './dateTimeFormat';
|
|
23
|
+
export * from './dateRangeLimit';
|
|
23
24
|
export * from './sortingRules';
|
|
24
25
|
export * from './dataLoadingMode';
|
|
25
26
|
export * from './renderMode';
|
|
@@ -51,11 +51,7 @@ import {
|
|
|
51
51
|
getCollectionFromModel,
|
|
52
52
|
isToManyAssociationField,
|
|
53
53
|
} from '../internal/utils/modelUtils';
|
|
54
|
-
import {
|
|
55
|
-
namePathToPathKey,
|
|
56
|
-
parsePathString,
|
|
57
|
-
resolveDynamicNamePath,
|
|
58
|
-
} from '../models/blocks/form/value-runtime/path';
|
|
54
|
+
import { namePathToPathKey, parsePathString, resolveDynamicNamePath } from '../models/blocks/form/value-runtime/path';
|
|
59
55
|
import { ensureFormValueDrivenLinkageRefresh } from './linkageRulesFormValueRefresh';
|
|
60
56
|
|
|
61
57
|
interface LinkageRule {
|
|
@@ -1759,7 +1755,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
1759
1755
|
|
|
1760
1756
|
const linkageRules: LinkageRule[] = params.value as LinkageRule[];
|
|
1761
1757
|
const allModels: FlowModel[] = ctx.model.__allModels || (ctx.model.__allModels = []);
|
|
1762
|
-
const directValuePatches: Array<{ path:
|
|
1758
|
+
const directValuePatches: Array<{ path: Array<string | number>; value: any; whenEmpty?: boolean }> = [];
|
|
1763
1759
|
const rootCollection = getCollectionFromModel((ctx.model as any)?.context?.blockModel ?? ctx.model);
|
|
1764
1760
|
const isSafeToWriteAssociationSubpath = (namePath: any): boolean => {
|
|
1765
1761
|
if (!Array.isArray(namePath) || !namePath.length) return true;
|
|
@@ -1800,6 +1796,24 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
1800
1796
|
const fieldIndex = (ctx.model as any)?.context?.fieldIndex;
|
|
1801
1797
|
return resolveDynamicNamePath(path, fieldIndex);
|
|
1802
1798
|
};
|
|
1799
|
+
const getDefaultPatchRuntime = () => {
|
|
1800
|
+
const blockModel = (ctx.model as any)?.context?.blockModel ?? ctx.model;
|
|
1801
|
+
return blockModel?.formValueRuntime ?? (ctx as any)?.formValueRuntime;
|
|
1802
|
+
};
|
|
1803
|
+
const rememberAppliedDefaultPatches = (patches: typeof directValuePatches) => {
|
|
1804
|
+
const runtime = getDefaultPatchRuntime();
|
|
1805
|
+
if (typeof runtime?.recordDefaultValuePatch !== 'function') return;
|
|
1806
|
+
|
|
1807
|
+
const lastPatchByPathKey = new Map<string, (typeof directValuePatches)[number]>();
|
|
1808
|
+
for (const patch of patches) {
|
|
1809
|
+
lastPatchByPathKey.set(namePathToPathKey(patch.path), patch);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
for (const patch of lastPatchByPathKey.values()) {
|
|
1813
|
+
if (!patch.whenEmpty) continue;
|
|
1814
|
+
runtime.recordDefaultValuePatch(patch.path, patch.value);
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1803
1817
|
const addFormValuePatch = (patch: { path: any; value: any; whenEmpty?: boolean }) => {
|
|
1804
1818
|
if (!patch) return;
|
|
1805
1819
|
const path = (patch as any)?.path;
|
|
@@ -1823,20 +1837,33 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
1823
1837
|
return;
|
|
1824
1838
|
}
|
|
1825
1839
|
const whenEmpty = !!(patch as any)?.whenEmpty;
|
|
1840
|
+
const value = (patch as any)?.value;
|
|
1826
1841
|
try {
|
|
1827
1842
|
const form = ctx.model?.context?.form;
|
|
1828
1843
|
const current = form?.getFieldValue?.(resolvedPath);
|
|
1829
|
-
if (whenEmpty
|
|
1830
|
-
|
|
1844
|
+
if (whenEmpty) {
|
|
1845
|
+
const runtime = getDefaultPatchRuntime();
|
|
1846
|
+
if (typeof runtime?.canApplyDefaultValuePatch === 'function') {
|
|
1847
|
+
const canApply = runtime.canApplyDefaultValuePatch(resolvedPath, value);
|
|
1848
|
+
if (!canApply) {
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
} else if (typeof current !== 'undefined' && current !== null && current !== '') {
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1831
1854
|
}
|
|
1832
|
-
if (_.isEqual(current,
|
|
1855
|
+
if (_.isEqual(current, value)) {
|
|
1833
1856
|
return;
|
|
1834
1857
|
}
|
|
1835
1858
|
} catch {
|
|
1836
1859
|
// ignore
|
|
1837
1860
|
}
|
|
1838
1861
|
|
|
1839
|
-
directValuePatches.push({
|
|
1862
|
+
directValuePatches.push({
|
|
1863
|
+
path: resolvedPath,
|
|
1864
|
+
value,
|
|
1865
|
+
...(whenEmpty ? { whenEmpty: true } : {}),
|
|
1866
|
+
});
|
|
1840
1867
|
};
|
|
1841
1868
|
|
|
1842
1869
|
const getModelTargetPathForPatch = (model: any): string | null => {
|
|
@@ -2018,6 +2045,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2018
2045
|
if (typeof directSetter === 'function') {
|
|
2019
2046
|
try {
|
|
2020
2047
|
await trySetFormValues(directSetter, directCtx, 'linkage');
|
|
2048
|
+
rememberAppliedDefaultPatches(allPatches);
|
|
2021
2049
|
return;
|
|
2022
2050
|
} catch (error) {
|
|
2023
2051
|
console.warn('[linkageRules] Failed to set form values via setFormValues', {
|
|
@@ -2034,6 +2062,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2034
2062
|
if (typeof blockSetter === 'function') {
|
|
2035
2063
|
try {
|
|
2036
2064
|
await trySetFormValues(blockSetter, blockCtx, 'linkage');
|
|
2065
|
+
rememberAppliedDefaultPatches(allPatches);
|
|
2037
2066
|
return;
|
|
2038
2067
|
} catch (error) {
|
|
2039
2068
|
console.warn('[linkageRules] Failed to set form values via setFormValues', {
|
|
@@ -2063,6 +2092,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2063
2092
|
console.warn('[linkageRules] Failed to set form field value (fallback setFieldValue)', { path }, error);
|
|
2064
2093
|
}
|
|
2065
2094
|
});
|
|
2095
|
+
rememberAppliedDefaultPatches(allPatches);
|
|
2066
2096
|
};
|
|
2067
2097
|
|
|
2068
2098
|
export const blockLinkageRules = defineAction({
|
|
@@ -2348,33 +2378,95 @@ export const fieldLinkageRules = defineAction({
|
|
|
2348
2378
|
return;
|
|
2349
2379
|
}
|
|
2350
2380
|
|
|
2351
|
-
const
|
|
2381
|
+
const getRowFieldIndexInfoFromModel = (model: any) => {
|
|
2352
2382
|
const fieldIndex = model?.context?.fieldIndex;
|
|
2353
2383
|
const arr = Array.isArray(fieldIndex) ? fieldIndex : [];
|
|
2354
|
-
|
|
2384
|
+
const normalized = arr.filter((it): it is string => typeof it === 'string');
|
|
2355
2385
|
const entries: Array<{ name: string; index: number }> = [];
|
|
2356
|
-
|
|
2357
|
-
|
|
2386
|
+
const path: Array<string | number> = [];
|
|
2387
|
+
for (const it of normalized) {
|
|
2358
2388
|
const [name, indexStr] = it.split(':');
|
|
2359
2389
|
const index = Number(indexStr);
|
|
2360
2390
|
if (!name || Number.isNaN(index)) continue;
|
|
2361
2391
|
entries.push({ name, index });
|
|
2392
|
+
path.push(name, index);
|
|
2362
2393
|
}
|
|
2394
|
+
return { normalized, entries, path };
|
|
2395
|
+
};
|
|
2396
|
+
|
|
2397
|
+
const getRowScopeKeyFromModel = (model: any): string | null => {
|
|
2398
|
+
const { entries } = getRowFieldIndexInfoFromModel(model);
|
|
2363
2399
|
if (!entries.length) return null;
|
|
2364
2400
|
const deepest = entries[entries.length - 1].name;
|
|
2365
2401
|
const occurrence = entries.reduce((count, e) => (e.name === deepest ? count + 1 : count), 0);
|
|
2366
2402
|
return `${deepest}#${occurrence}`;
|
|
2367
2403
|
};
|
|
2368
2404
|
|
|
2405
|
+
const getRowFieldIndexKeyFromModel = (model: any): string | null => {
|
|
2406
|
+
const { normalized } = getRowFieldIndexInfoFromModel(model);
|
|
2407
|
+
if (!normalized.length) return null;
|
|
2408
|
+
return JSON.stringify(normalized);
|
|
2409
|
+
};
|
|
2410
|
+
|
|
2411
|
+
const getRowPathFromModel = (model: any): Array<string | number> | null => {
|
|
2412
|
+
const { path } = getRowFieldIndexInfoFromModel(model);
|
|
2413
|
+
return path.length ? path : null;
|
|
2414
|
+
};
|
|
2415
|
+
|
|
2416
|
+
const getFormForRowFork = (model: any) => {
|
|
2417
|
+
return (
|
|
2418
|
+
model?.context?.form ??
|
|
2419
|
+
model?.context?.blockModel?.context?.form ??
|
|
2420
|
+
ctx.model?.context?.form ??
|
|
2421
|
+
ctx.model?.context?.blockModel?.context?.form
|
|
2422
|
+
);
|
|
2423
|
+
};
|
|
2424
|
+
|
|
2425
|
+
const isRowForkMountedInCurrentValue = (model: any): boolean => {
|
|
2426
|
+
const rowPath = getRowPathFromModel(model);
|
|
2427
|
+
if (!rowPath) return true;
|
|
2428
|
+
const form = getFormForRowFork(model);
|
|
2429
|
+
if (!form || typeof form.getFieldValue !== 'function') return true;
|
|
2430
|
+
return typeof form.getFieldValue(rowPath as any) !== 'undefined';
|
|
2431
|
+
};
|
|
2432
|
+
|
|
2433
|
+
const hasRowItemContext = (model: any): boolean => {
|
|
2434
|
+
const itemOptions = model?.context?.getPropertyOptions?.('item');
|
|
2435
|
+
if (itemOptions) return true;
|
|
2436
|
+
return typeof model?.context?.item !== 'undefined';
|
|
2437
|
+
};
|
|
2438
|
+
|
|
2439
|
+
const hasSubTableRowMarker = (model: any): boolean => {
|
|
2440
|
+
const markerOptions = model?.context?.getPropertyOptions?.('subTableRowFork');
|
|
2441
|
+
if (markerOptions) return true;
|
|
2442
|
+
return (
|
|
2443
|
+
typeof model?.context?.subTableRowFork !== 'undefined' ||
|
|
2444
|
+
typeof model?.subTableRowFork !== 'undefined' ||
|
|
2445
|
+
model?.subTableRowFork === true
|
|
2446
|
+
);
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2369
2449
|
const isRowGridForkModel = (model: any): boolean => {
|
|
2370
2450
|
if (!model || typeof model !== 'object') return false;
|
|
2371
2451
|
if ((model as any)?.subModels?.field) return false;
|
|
2372
2452
|
if (!(model as any)?.subModels?.items) return false;
|
|
2373
|
-
return !!getRowScopeKeyFromModel(model);
|
|
2453
|
+
return !!getRowScopeKeyFromModel(model) && !!getRowFieldIndexKeyFromModel(model);
|
|
2454
|
+
};
|
|
2455
|
+
|
|
2456
|
+
const isSubTableRowForkModel = (model: any): boolean => {
|
|
2457
|
+
if (!model || typeof model !== 'object') return false;
|
|
2458
|
+
if (!hasSubTableRowMarker(model)) return false;
|
|
2459
|
+
if (!getRowScopeKeyFromModel(model) || !getRowFieldIndexKeyFromModel(model)) return false;
|
|
2460
|
+
return hasRowItemContext(model);
|
|
2461
|
+
};
|
|
2462
|
+
|
|
2463
|
+
const isRowScopedForkModel = (model: any): boolean => {
|
|
2464
|
+
return isRowGridForkModel(model) || isSubTableRowForkModel(model);
|
|
2374
2465
|
};
|
|
2375
2466
|
|
|
2376
|
-
const
|
|
2467
|
+
const collectRowScopedForksByKey = (): Map<string, FlowModel[]> => {
|
|
2377
2468
|
const out = new Map<string, FlowModel[]>();
|
|
2469
|
+
const seenByKey = new Map<string, Set<string>>();
|
|
2378
2470
|
const engine = ctx.engine;
|
|
2379
2471
|
if (!engine?.forEachModel) return out;
|
|
2380
2472
|
|
|
@@ -2383,9 +2475,15 @@ export const fieldLinkageRules = defineAction({
|
|
|
2383
2475
|
if (!forks || typeof forks.forEach !== 'function') return;
|
|
2384
2476
|
forks.forEach((fork: any) => {
|
|
2385
2477
|
if (!fork || fork.disposed) return;
|
|
2386
|
-
if (!
|
|
2478
|
+
if (!isRowScopedForkModel(fork)) return;
|
|
2479
|
+
if (!isRowForkMountedInCurrentValue(fork)) return;
|
|
2387
2480
|
const rowScopeKey = getRowScopeKeyFromModel(fork);
|
|
2388
|
-
|
|
2481
|
+
const fieldIndexKey = getRowFieldIndexKeyFromModel(fork);
|
|
2482
|
+
if (!rowScopeKey || !fieldIndexKey) return;
|
|
2483
|
+
const seen = seenByKey.get(rowScopeKey) || new Set<string>();
|
|
2484
|
+
if (seen.has(fieldIndexKey)) return;
|
|
2485
|
+
seen.add(fieldIndexKey);
|
|
2486
|
+
seenByKey.set(rowScopeKey, seen);
|
|
2389
2487
|
const arr = out.get(rowScopeKey) || [];
|
|
2390
2488
|
arr.push(fork as FlowModel);
|
|
2391
2489
|
out.set(rowScopeKey, arr);
|
|
@@ -2396,7 +2494,7 @@ export const fieldLinkageRules = defineAction({
|
|
|
2396
2494
|
};
|
|
2397
2495
|
|
|
2398
2496
|
const runRowScoped = async (): Promise<boolean> => {
|
|
2399
|
-
const forksByKey =
|
|
2497
|
+
const forksByKey = collectRowScopedForksByKey();
|
|
2400
2498
|
let hasAnyRowFork = false;
|
|
2401
2499
|
for (const [rowScopeKey, rowParams] of rowParamsByKey.entries()) {
|
|
2402
2500
|
const forks = forksByKey.get(rowScopeKey) || [];
|
|
@@ -345,7 +345,8 @@ export const openView = defineAction({
|
|
|
345
345
|
target: ctx.inputArgs.target || ctx.layoutContentElement,
|
|
346
346
|
dataSourceKey: runtimeDataSourceKey ?? actionDefaults.dataSourceKey,
|
|
347
347
|
collectionName: runtimeCollectionName ?? actionDefaults.collectionName,
|
|
348
|
-
associationName:
|
|
348
|
+
associationName:
|
|
349
|
+
typeof runtimeAssociationName !== 'undefined' ? runtimeAssociationName : actionDefaults.associationName,
|
|
349
350
|
filterByTk: mergedFilterByTk,
|
|
350
351
|
sourceId: mergedSourceId,
|
|
351
352
|
tabUid: mergedTabUid,
|
|
@@ -7,9 +7,25 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { defineAction, tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import { DetailsItemModel } from '../models/blocks/details/DetailsItemModel';
|
|
12
|
-
import { rebuildFieldSubModel } from '../internal/utils/rebuildFieldSubModel';
|
|
12
|
+
import { getFieldBindingUse, rebuildFieldSubModel } from '../internal/utils/rebuildFieldSubModel';
|
|
13
|
+
|
|
14
|
+
type PatternAwareFieldModelMeta = {
|
|
15
|
+
preserveOnPatternChange?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type PatternAwareFieldModel = {
|
|
19
|
+
scheduleApplyJsSettings?: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function shouldPreserveFieldModelOnPatternChange(ctx: any) {
|
|
23
|
+
const fieldModel = ctx.model.subModels.field;
|
|
24
|
+
const fieldUse = getFieldBindingUse(fieldModel) ?? fieldModel?.use;
|
|
25
|
+
const ModelClass = typeof fieldUse === 'string' ? ctx.engine.getModelClass(fieldUse) : fieldUse;
|
|
26
|
+
|
|
27
|
+
return ((ModelClass?.meta as PatternAwareFieldModelMeta | undefined)?.preserveOnPatternChange ?? false) === true;
|
|
28
|
+
}
|
|
13
29
|
|
|
14
30
|
export const pattern = defineAction({
|
|
15
31
|
name: 'pattern',
|
|
@@ -56,6 +72,13 @@ export const pattern = defineAction({
|
|
|
56
72
|
};
|
|
57
73
|
},
|
|
58
74
|
afterParamsSave: async (ctx: any, params, previousParams) => {
|
|
75
|
+
if (shouldPreserveFieldModelOnPatternChange(ctx)) {
|
|
76
|
+
if (params.pattern !== previousParams.pattern) {
|
|
77
|
+
(ctx.model.subModels.field as PatternAwareFieldModel | undefined)?.scheduleApplyJsSettings?.();
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
59
82
|
const targetCollection = ctx.collectionField.targetCollection;
|
|
60
83
|
const targetCollectionTitleField = targetCollection?.getField(
|
|
61
84
|
ctx.model.subModels.field.props?.fieldNames?.label || ctx.model.props.titleField,
|
|
@@ -47,6 +47,8 @@ interface RouteLike {
|
|
|
47
47
|
pathname?: string;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
const hasUsableSourceId = (sourceId: unknown) => sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
|
|
51
|
+
|
|
50
52
|
/**
|
|
51
53
|
* 管理 admin 场景下每个 page 的 v2 视图栈编排。
|
|
52
54
|
* 该协调器只负责状态机和开关视图,不直接绑定 React 生命周期。
|
|
@@ -264,6 +266,10 @@ export class AdminLayoutRouteCoordinator {
|
|
|
264
266
|
const destroyRef = React.createRef<(result?: any, force?: boolean) => void>();
|
|
265
267
|
const updateRef = React.createRef<(value: any) => void>();
|
|
266
268
|
const openViewParams = getOpenViewStepParams(viewItem.model);
|
|
269
|
+
const associationName =
|
|
270
|
+
openViewParams?.associationName && !hasUsableSourceId(viewItem.params.sourceId)
|
|
271
|
+
? null
|
|
272
|
+
: openViewParams?.associationName;
|
|
267
273
|
const openerUids = viewList.slice(0, viewItem.index).map((item) => item.params.viewUid);
|
|
268
274
|
const navigation = new ViewNavigation(
|
|
269
275
|
this.flowEngine.context,
|
|
@@ -273,7 +279,7 @@ export class AdminLayoutRouteCoordinator {
|
|
|
273
279
|
viewItem.model.dispatchEvent('click', {
|
|
274
280
|
target: runtime.meta.layoutContentElement || this.layoutContentElement,
|
|
275
281
|
collectionName: openViewParams?.collectionName,
|
|
276
|
-
associationName
|
|
282
|
+
associationName,
|
|
277
283
|
dataSourceKey: openViewParams?.dataSourceKey,
|
|
278
284
|
destroyRef,
|
|
279
285
|
updateRef,
|