@nocobase/client-v2 2.1.0-alpha.30 → 2.1.0-alpha.32
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/actions/linkageRulesFormValueRefresh.d.ts +10 -0
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/actions/AssociateActionModel.d.ts +19 -0
- package/es/flow/models/actions/AssociationActionUtils.d.ts +17 -0
- package/es/flow/models/actions/DisassociateActionModel.d.ts +16 -0
- package/es/flow/models/actions/index.d.ts +3 -0
- package/es/flow/models/base/GridModel.d.ts +3 -1
- 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/filter-form/FilterFormGridModel.d.ts +15 -6
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +7 -0
- package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +3 -0
- package/es/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.d.ts +9 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +20 -0
- package/es/flow/models/fields/JSEditableFieldModel.d.ts +4 -0
- package/es/flow-compat/data.d.ts +9 -2
- package/es/flow-compat/index.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +100 -93
- package/lib/index.js +101 -94
- package/package.json +6 -5
- package/src/BaseApplication.tsx +1 -1
- package/src/__tests__/app.test.tsx +23 -6
- package/src/__tests__/globalDeps.test.ts +5 -0
- 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__/linkageRules.formValueDrivenRefresh.test.ts +438 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +42 -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 +119 -14
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +492 -0
- package/src/flow/actions/linkageRulesRefresh.tsx +4 -2
- package/src/flow/actions/openView.tsx +2 -1
- package/src/flow/actions/pattern.tsx +25 -2
- package/src/flow/actions/titleField.tsx +8 -3
- package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +7 -1
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +117 -0
- package/src/flow/components/FieldAssignValueInput.tsx +1 -0
- package/src/flow/components/filter/LinkageFilterItem.tsx +6 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +14 -13
- package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +33 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -5
- package/src/flow/index.ts +1 -0
- package/src/flow/internal/utils/__tests__/titleFieldQuickSync.test.ts +1 -0
- package/src/flow/internal/utils/titleFieldQuickSync.ts +2 -2
- package/src/flow/models/actions/AssociateActionModel.tsx +196 -0
- package/src/flow/models/actions/AssociationActionUtils.ts +90 -0
- package/src/flow/models/actions/DisassociateActionModel.tsx +57 -0
- package/src/flow/models/actions/FilterActionModel.tsx +17 -9
- package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +250 -0
- package/src/flow/models/actions/index.ts +3 -0
- package/src/flow/models/base/GridModel.tsx +21 -1
- 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/base/__tests__/GridModel.dragSnapshotContainer.test.ts +98 -0
- package/src/flow/models/blocks/details/DetailsItemModel.tsx +3 -0
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +200 -36
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +270 -1
- package/src/flow/models/blocks/filter-form/__tests__/customFieldOperators.test.tsx +23 -0
- package/src/flow/models/blocks/filter-form/customFieldOperators.ts +12 -1
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +22 -8
- package/src/flow/models/blocks/filter-form/fields/__tests__/FilterFormCustomFieldModel.recordSelect.test.tsx +18 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +51 -1
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +75 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +48 -28
- 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/blocks/shared/filterOperators.ts +14 -0
- package/src/flow/models/blocks/table/TableBlockModel.tsx +19 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +5 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +48 -8
- 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/AssociationFieldModel/recordSelectSettingsUtils.ts +20 -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/DividerItemModel.tsx +30 -15
- 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
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +11 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +235 -0
- package/src/flow-compat/data.ts +25 -3
- package/src/flow-compat/index.ts +7 -1
- package/src/index.ts +1 -0
- package/src/utils/globalDeps.ts +6 -0
|
@@ -0,0 +1,492 @@
|
|
|
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 {
|
|
11
|
+
extractUsedVariablePathsFromRunJS,
|
|
12
|
+
FlowContext,
|
|
13
|
+
FlowModel,
|
|
14
|
+
FlowRuntimeContext,
|
|
15
|
+
isRunJSValue,
|
|
16
|
+
} from '@nocobase/flow-engine';
|
|
17
|
+
import _ from 'lodash';
|
|
18
|
+
import {
|
|
19
|
+
namePathToPathKey,
|
|
20
|
+
parsePathString,
|
|
21
|
+
pathKeyToNamePath,
|
|
22
|
+
} from '../models/blocks/form/value-runtime/path';
|
|
23
|
+
import {
|
|
24
|
+
collectStaticDepsFromRunJSValue,
|
|
25
|
+
collectStaticDepsFromTemplateValue,
|
|
26
|
+
recordDep,
|
|
27
|
+
type DepCollector,
|
|
28
|
+
} from '../models/blocks/form/value-runtime/deps';
|
|
29
|
+
import { linkageRulesRefresh } from './linkageRulesRefresh';
|
|
30
|
+
|
|
31
|
+
type NamePath = Array<string | number>;
|
|
32
|
+
|
|
33
|
+
type LinkageRefreshDeps = {
|
|
34
|
+
wildcard: boolean;
|
|
35
|
+
valuePaths: NamePath[];
|
|
36
|
+
structuralPaths: NamePath[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type LinkageRefreshBinding = {
|
|
40
|
+
signature: string;
|
|
41
|
+
running: boolean;
|
|
42
|
+
linkageTxIds: Set<string>;
|
|
43
|
+
pendingPayload: any;
|
|
44
|
+
dispose: () => void;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type FieldIndexEntry = {
|
|
48
|
+
name: string;
|
|
49
|
+
index: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const FORM_VALUES_CHANGE_EVENT = 'formValuesChange';
|
|
53
|
+
const LINKAGE_REFRESH_BINDINGS_KEY = '__formValueDrivenLinkageRefreshBindings';
|
|
54
|
+
|
|
55
|
+
function isSameNamePath(a: NamePath, b: NamePath) {
|
|
56
|
+
return a.length === b.length && a.every((seg, index) => seg === b[index]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isNamePathPrefix(prefix: NamePath, path: NamePath) {
|
|
60
|
+
if (prefix.length > path.length) return false;
|
|
61
|
+
return prefix.every((seg, index) => seg === path[index]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function dedupeNamePaths(paths: NamePath[]) {
|
|
65
|
+
const byKey = new Map<string, NamePath>();
|
|
66
|
+
for (const path of paths) {
|
|
67
|
+
if (!path?.length) continue;
|
|
68
|
+
byKey.set(namePathToPathKey(path), path);
|
|
69
|
+
}
|
|
70
|
+
return Array.from(byKey.values());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function minimizeValueNamePaths(paths: NamePath[]) {
|
|
74
|
+
const deduped = dedupeNamePaths(paths);
|
|
75
|
+
return deduped.filter((path, index) => {
|
|
76
|
+
return !deduped.some((other, otherIndex) => otherIndex !== index && isNamePathPrefix(path, other));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseFieldIndexEntries(fieldIndex: unknown): FieldIndexEntry[] {
|
|
81
|
+
const arr = Array.isArray(fieldIndex) ? fieldIndex : [];
|
|
82
|
+
const entries: FieldIndexEntry[] = [];
|
|
83
|
+
for (const it of arr) {
|
|
84
|
+
if (typeof it !== 'string') continue;
|
|
85
|
+
const [name, indexStr] = it.split(':');
|
|
86
|
+
const index = Number(indexStr);
|
|
87
|
+
if (!name || Number.isNaN(index)) continue;
|
|
88
|
+
entries.push({ name, index });
|
|
89
|
+
}
|
|
90
|
+
return entries;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getFieldIndexEntriesFromContext(ctx: any): FieldIndexEntry[] {
|
|
94
|
+
return parseFieldIndexEntries(ctx?.model?.context?.fieldIndex ?? ctx?.fieldIndex);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildItemRowPath(entries: FieldIndexEntry[], parentDepth: number): NamePath | null {
|
|
98
|
+
const targetIndex = entries.length - 1 - parentDepth;
|
|
99
|
+
if (targetIndex < 0) return null;
|
|
100
|
+
|
|
101
|
+
const out: NamePath = [];
|
|
102
|
+
for (let i = 0; i <= targetIndex; i++) {
|
|
103
|
+
out.push(entries[i].name, entries[i].index);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildItemListRootPath(entries: FieldIndexEntry[], parentDepth: number): NamePath | null {
|
|
109
|
+
const rowPath = buildItemRowPath(entries, parentDepth);
|
|
110
|
+
if (!rowPath?.length) return null;
|
|
111
|
+
return rowPath.slice(0, -1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveItemDependencyPath(ctx: FlowContext, depPath: NamePath): LinkageRefreshDeps {
|
|
115
|
+
const entries = getFieldIndexEntriesFromContext(ctx as any);
|
|
116
|
+
if (!entries.length) {
|
|
117
|
+
return { wildcard: true, valuePaths: [], structuralPaths: [] };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let parentDepth = 0;
|
|
121
|
+
let cursor = [...depPath];
|
|
122
|
+
while (cursor[0] === 'parentItem') {
|
|
123
|
+
parentDepth += 1;
|
|
124
|
+
cursor = cursor.slice(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const head = cursor[0];
|
|
128
|
+
if (!head) {
|
|
129
|
+
return { wildcard: true, valuePaths: [], structuralPaths: [] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (head === 'value') {
|
|
133
|
+
const rowPath = buildItemRowPath(entries, parentDepth);
|
|
134
|
+
if (!rowPath) return { wildcard: true, valuePaths: [], structuralPaths: [] };
|
|
135
|
+
const listRootPath = buildItemListRootPath(entries, parentDepth);
|
|
136
|
+
return {
|
|
137
|
+
wildcard: false,
|
|
138
|
+
valuePaths: [[...rowPath, ...cursor.slice(1)]],
|
|
139
|
+
structuralPaths: listRootPath ? [listRootPath] : [],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (head === 'index' || head === 'length') {
|
|
144
|
+
const listRootPath = buildItemListRootPath(entries, parentDepth);
|
|
145
|
+
if (!listRootPath) return { wildcard: true, valuePaths: [], structuralPaths: [] };
|
|
146
|
+
return {
|
|
147
|
+
wildcard: false,
|
|
148
|
+
valuePaths: [],
|
|
149
|
+
structuralPaths: [listRootPath],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (head === '__is_new__' || head === '__is_stored__') {
|
|
154
|
+
const rowPath = buildItemRowPath(entries, parentDepth);
|
|
155
|
+
if (!rowPath) return { wildcard: true, valuePaths: [], structuralPaths: [] };
|
|
156
|
+
return {
|
|
157
|
+
wildcard: false,
|
|
158
|
+
valuePaths: [[...rowPath, head]],
|
|
159
|
+
structuralPaths: [],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { wildcard: true, valuePaths: [], structuralPaths: [] };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function addRunjsUsageToCollector(script: string, collector: DepCollector) {
|
|
167
|
+
if (typeof script !== 'string' || !script.trim()) return;
|
|
168
|
+
const usage = extractUsedVariablePathsFromRunJS(script) || {};
|
|
169
|
+
for (const [varName, rawPaths] of Object.entries(usage)) {
|
|
170
|
+
const paths = Array.isArray(rawPaths) ? rawPaths : [];
|
|
171
|
+
const normalized = paths.length ? paths : [''];
|
|
172
|
+
for (const subPath of normalized) {
|
|
173
|
+
if (varName === 'formValues') {
|
|
174
|
+
if (!subPath) {
|
|
175
|
+
collector.wildcard = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const segs = parsePathString(String(subPath)).filter((seg) => typeof seg !== 'object') as NamePath;
|
|
179
|
+
recordDep(segs, collector);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (varName === 'item') {
|
|
183
|
+
const key = subPath ? `ctx:item:${String(subPath)}` : 'ctx:item';
|
|
184
|
+
collector.deps.add(key);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function collectRunjsDepsFromLinkageRules(params: any, collector: DepCollector) {
|
|
191
|
+
const seen = new WeakSet<object>();
|
|
192
|
+
const visit = (value: any) => {
|
|
193
|
+
if (!value || typeof value !== 'object') return;
|
|
194
|
+
if (seen.has(value)) return;
|
|
195
|
+
seen.add(value);
|
|
196
|
+
|
|
197
|
+
if (isRunJSValue(value)) {
|
|
198
|
+
collectStaticDepsFromRunJSValue(value, collector);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (Array.isArray(value)) {
|
|
202
|
+
value.forEach(visit);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const actionName = (value as any)?.name;
|
|
207
|
+
if (actionName === 'linkageRunjs' || actionName === 'runjs') {
|
|
208
|
+
addRunjsUsageToCollector(_.get(value, ['params', 'value', 'script']), collector);
|
|
209
|
+
addRunjsUsageToCollector(_.get(value, ['params', 'code']), collector);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
Object.values(value).forEach(visit);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
visit(params);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function collectLinkageRefreshDeps(ctx: FlowContext, params: any): LinkageRefreshDeps {
|
|
219
|
+
const collector: DepCollector = { deps: new Set(), wildcard: false };
|
|
220
|
+
|
|
221
|
+
collectStaticDepsFromTemplateValue(params, collector);
|
|
222
|
+
collectRunjsDepsFromLinkageRules(params, collector);
|
|
223
|
+
|
|
224
|
+
const valuePaths: NamePath[] = [];
|
|
225
|
+
const structuralPaths: NamePath[] = [];
|
|
226
|
+
let wildcard = collector.wildcard;
|
|
227
|
+
|
|
228
|
+
for (const depKey of collector.deps) {
|
|
229
|
+
if (depKey === 'fv:*') {
|
|
230
|
+
wildcard = true;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (depKey.startsWith('fv:')) {
|
|
235
|
+
const inner = depKey.slice('fv:'.length);
|
|
236
|
+
if (!inner) {
|
|
237
|
+
wildcard = true;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
valuePaths.push(pathKeyToNamePath(inner));
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (depKey === 'ctx:item' || depKey.startsWith('ctx:item:')) {
|
|
245
|
+
const subPath = depKey === 'ctx:item' ? '' : depKey.slice('ctx:item:'.length);
|
|
246
|
+
const depPath = subPath
|
|
247
|
+
? (parsePathString(subPath).filter((seg) => typeof seg !== 'object') as NamePath)
|
|
248
|
+
: [];
|
|
249
|
+
const resolved = resolveItemDependencyPath(ctx, depPath);
|
|
250
|
+
wildcard ||= resolved.wildcard;
|
|
251
|
+
valuePaths.push(...resolved.valuePaths);
|
|
252
|
+
structuralPaths.push(...resolved.structuralPaths);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
wildcard,
|
|
258
|
+
valuePaths: minimizeValueNamePaths(valuePaths),
|
|
259
|
+
structuralPaths: dedupeNamePaths(structuralPaths),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function hasLinkageRefreshDeps(deps: LinkageRefreshDeps) {
|
|
264
|
+
return deps.wildcard || deps.valuePaths.length > 0 || deps.structuralPaths.length > 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getChangedPathsFromPayload(payload: any): NamePath[] {
|
|
268
|
+
const rawChangedPaths = Array.isArray(payload?.changedPaths) ? payload.changedPaths : [];
|
|
269
|
+
const out: NamePath[] = [];
|
|
270
|
+
|
|
271
|
+
for (const path of rawChangedPaths) {
|
|
272
|
+
if (Array.isArray(path)) {
|
|
273
|
+
if (path.length === 1 && typeof path[0] === 'string') {
|
|
274
|
+
const namePath = pathKeyToNamePath(path[0]);
|
|
275
|
+
if (namePath.length) out.push(namePath);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const segs = path.filter((seg) => typeof seg === 'string' || typeof seg === 'number') as NamePath;
|
|
279
|
+
if (segs.length) out.push(segs);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (typeof path === 'string' && path) {
|
|
283
|
+
out.push(pathKeyToNamePath(path));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (out.length) {
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const changedValues = payload?.changedValues;
|
|
292
|
+
if (changedValues && typeof changedValues === 'object') {
|
|
293
|
+
for (const key of Object.keys(changedValues)) {
|
|
294
|
+
const namePath = pathKeyToNamePath(key);
|
|
295
|
+
if (namePath.length) out.push(namePath);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function linkageRefreshDepsMatchPayload(deps: LinkageRefreshDeps, payload: any) {
|
|
303
|
+
if (!hasLinkageRefreshDeps(deps)) return false;
|
|
304
|
+
const changedPaths = getChangedPathsFromPayload(payload);
|
|
305
|
+
if (deps.wildcard) return true;
|
|
306
|
+
if (!changedPaths.length) return true;
|
|
307
|
+
|
|
308
|
+
for (const changedPath of changedPaths) {
|
|
309
|
+
for (const depPath of deps.valuePaths) {
|
|
310
|
+
if (isNamePathPrefix(depPath, changedPath) || isNamePathPrefix(changedPath, depPath)) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const depPath of deps.structuralPaths) {
|
|
316
|
+
if (isSameNamePath(depPath, changedPath) || isNamePathPrefix(changedPath, depPath)) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getDepsSignature(deps: LinkageRefreshDeps, formBlock: any) {
|
|
326
|
+
const toKeys = (paths: NamePath[]) => paths.map((path) => namePathToPathKey(path)).sort();
|
|
327
|
+
return JSON.stringify({
|
|
328
|
+
formBlockUid: formBlock?.uid,
|
|
329
|
+
wildcard: deps.wildcard,
|
|
330
|
+
valuePaths: toKeys(deps.valuePaths),
|
|
331
|
+
structuralPaths: toKeys(deps.structuralPaths),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getLinkageRefreshBindings(model: any): Map<string, LinkageRefreshBinding> {
|
|
336
|
+
return (model[LINKAGE_REFRESH_BINDINGS_KEY] ||= new Map<string, LinkageRefreshBinding>());
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function isFormBlockForLinkageRefresh(model: any) {
|
|
340
|
+
if (!model || typeof model !== 'object') return false;
|
|
341
|
+
if (!model.emitter || typeof model.emitter.on !== 'function' || typeof model.emitter.off !== 'function') return false;
|
|
342
|
+
return !!model.formValueRuntime || !!model.context?.form || typeof model.context?.setFormValues === 'function';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function findFormBlockForLinkageRefresh(ctx: FlowContext): any | null {
|
|
346
|
+
const candidates: any[] = [];
|
|
347
|
+
const push = (model: any) => {
|
|
348
|
+
if (model && !candidates.includes(model)) candidates.push(model);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
push((ctx.model as any)?.context?.blockModel);
|
|
352
|
+
push(ctx.model);
|
|
353
|
+
|
|
354
|
+
let cursor: any = (ctx.model as any)?.parent;
|
|
355
|
+
while (cursor) {
|
|
356
|
+
push(cursor);
|
|
357
|
+
cursor = cursor?.parent;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return candidates.find(isFormBlockForLinkageRefresh) || null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function disposeLinkageRefreshBinding(model: any, key: string) {
|
|
364
|
+
const bindings = getLinkageRefreshBindings(model);
|
|
365
|
+
const existing = bindings.get(key);
|
|
366
|
+
if (existing) {
|
|
367
|
+
existing.dispose();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function ensureFormValueDrivenLinkageRefresh(ctx: FlowContext, params: any, actionName: string) {
|
|
372
|
+
const model: any = ctx.model;
|
|
373
|
+
const flowKey = (ctx as any)?.flowKey;
|
|
374
|
+
if (!model || !flowKey) return;
|
|
375
|
+
|
|
376
|
+
const stepKey = 'linkageRules';
|
|
377
|
+
const bindingKey = `${flowKey}:${stepKey}:${actionName}`;
|
|
378
|
+
const bindings = getLinkageRefreshBindings(model);
|
|
379
|
+
const deps = collectLinkageRefreshDeps(ctx, params);
|
|
380
|
+
if (!hasLinkageRefreshDeps(deps)) {
|
|
381
|
+
disposeLinkageRefreshBinding(model, bindingKey);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const formBlock = findFormBlockForLinkageRefresh(ctx);
|
|
386
|
+
if (!formBlock) {
|
|
387
|
+
disposeLinkageRefreshBinding(model, bindingKey);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const signature = getDepsSignature(deps, formBlock);
|
|
392
|
+
const existing = bindings.get(bindingKey);
|
|
393
|
+
if (existing?.signature === signature) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (existing) {
|
|
397
|
+
existing.dispose();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const binding: LinkageRefreshBinding = {
|
|
401
|
+
signature,
|
|
402
|
+
running: false,
|
|
403
|
+
linkageTxIds: new Set(),
|
|
404
|
+
pendingPayload: null,
|
|
405
|
+
dispose: () => {},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const engineEmitter = model?.flowEngine?.emitter || (ctx as any)?.engine?.emitter || model?.context?.engine?.emitter;
|
|
409
|
+
|
|
410
|
+
const dispose = () => {
|
|
411
|
+
formBlock.emitter?.off?.(FORM_VALUES_CHANGE_EVENT, listener);
|
|
412
|
+
engineEmitter?.off?.('model:unmounted', cleanupOnUnmount);
|
|
413
|
+
engineEmitter?.off?.('model:destroyed', cleanupOnDestroyed);
|
|
414
|
+
if (bindings.get(bindingKey) === binding) {
|
|
415
|
+
bindings.delete(bindingKey);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const rememberLinkageTxId = (linkageTxId: unknown) => {
|
|
420
|
+
if (typeof linkageTxId !== 'string' || !linkageTxId) return;
|
|
421
|
+
binding.linkageTxIds.add(linkageTxId);
|
|
422
|
+
if (binding.linkageTxIds.size <= 20) return;
|
|
423
|
+
const oldest = binding.linkageTxIds.values().next().value;
|
|
424
|
+
if (oldest) binding.linkageTxIds.delete(oldest);
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const listener = (payload: any) => {
|
|
428
|
+
const payloadLinkageTxId = typeof payload?.linkageTxId === 'string' ? payload.linkageTxId : undefined;
|
|
429
|
+
if (payload?.source === 'linkage' && payloadLinkageTxId && binding.linkageTxIds.has(payloadLinkageTxId)) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (model.disposed || formBlock.disposed) {
|
|
433
|
+
dispose();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const latestDeps = collectLinkageRefreshDeps(ctx, params);
|
|
437
|
+
if (!linkageRefreshDepsMatchPayload(latestDeps, payload)) return;
|
|
438
|
+
if (binding.running) {
|
|
439
|
+
binding.pendingPayload = payload;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const refreshLinkageTxId = payloadLinkageTxId || (typeof payload?.txId === 'string' ? payload.txId : undefined);
|
|
444
|
+
rememberLinkageTxId(refreshLinkageTxId);
|
|
445
|
+
binding.running = true;
|
|
446
|
+
const refreshCtx = new FlowRuntimeContext(model, flowKey);
|
|
447
|
+
refreshCtx.defineProperty('inputArgs', {
|
|
448
|
+
value: {
|
|
449
|
+
...(payload || {}),
|
|
450
|
+
linkageTxId: refreshLinkageTxId,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
void linkageRulesRefresh
|
|
455
|
+
.handler(refreshCtx, {
|
|
456
|
+
actionName,
|
|
457
|
+
flowKey,
|
|
458
|
+
stepKey,
|
|
459
|
+
})
|
|
460
|
+
.catch((error) => {
|
|
461
|
+
console.warn('[linkageRules] Failed to refresh form value driven linkage rules', error);
|
|
462
|
+
})
|
|
463
|
+
.finally(() => {
|
|
464
|
+
binding.running = false;
|
|
465
|
+
const pendingPayload = binding.pendingPayload;
|
|
466
|
+
binding.pendingPayload = null;
|
|
467
|
+
if (pendingPayload) {
|
|
468
|
+
listener(pendingPayload);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const cleanupOnUnmount = ({ model: unmountedModel }: { model: FlowModel }) => {
|
|
474
|
+
// Action linkage may hide the action itself, which unmounts its renderer.
|
|
475
|
+
// Keep the watcher alive so later form changes can restore the action state.
|
|
476
|
+
if (unmountedModel === formBlock || (unmountedModel === model && model.disposed)) {
|
|
477
|
+
dispose();
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const cleanupOnDestroyed = ({ model: destroyedModel }: { model: FlowModel }) => {
|
|
482
|
+
if (destroyedModel === model || destroyedModel === formBlock) {
|
|
483
|
+
dispose();
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
binding.dispose = dispose;
|
|
488
|
+
bindings.set(bindingKey, binding);
|
|
489
|
+
formBlock.emitter.on(FORM_VALUES_CHANGE_EVENT, listener);
|
|
490
|
+
engineEmitter?.on?.('model:unmounted', cleanupOnUnmount);
|
|
491
|
+
engineEmitter?.on?.('model:destroyed', cleanupOnDestroyed);
|
|
492
|
+
}
|
|
@@ -65,10 +65,12 @@ export const linkageRulesRefresh = defineAction({
|
|
|
65
65
|
|
|
66
66
|
const run = (async () => {
|
|
67
67
|
const raw = model?.getStepParams?.(flowKey, stepKey);
|
|
68
|
-
const resolved = await ctx.resolveJsonTemplate({ value: [], ...(raw || {}) });
|
|
69
68
|
const action = ctx.getAction?.(actionName);
|
|
69
|
+
const paramsForAction = action?.useRawParams
|
|
70
|
+
? { value: [], ...(raw || {}) }
|
|
71
|
+
: await ctx.resolveJsonTemplate({ value: [], ...(raw || {}) });
|
|
70
72
|
if (action?.handler) {
|
|
71
|
-
await action.handler(ctx,
|
|
73
|
+
await action.handler(ctx, paramsForAction);
|
|
72
74
|
}
|
|
73
75
|
})();
|
|
74
76
|
|
|
@@ -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,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { defineAction, DisplayItemModel, FlowModelContext, tExpr } from '@nocobase/flow-engine';
|
|
11
|
-
import {
|
|
11
|
+
import { getFlowFieldInterfaceOptions, isTitleFieldInterface } from '../../flow-compat';
|
|
12
12
|
|
|
13
13
|
const normalizeFilterTargetKey = (filterTargetKey: any) => {
|
|
14
14
|
if (typeof filterTargetKey === 'string') {
|
|
@@ -37,10 +37,15 @@ export const titleField = defineAction({
|
|
|
37
37
|
title: tExpr('Title field'),
|
|
38
38
|
uiMode: (ctx) => {
|
|
39
39
|
const targetCollection = ctx.collectionField.targetCollection;
|
|
40
|
-
const dataSourceManager =
|
|
40
|
+
const dataSourceManager =
|
|
41
|
+
ctx.dataSourceManager || ctx.model?.context?.dataSourceManager || ctx.app?.dataSourceManager;
|
|
41
42
|
const targetFields = targetCollection?.getFields?.() ?? [];
|
|
42
43
|
const options = targetFields
|
|
43
|
-
.filter((field) =>
|
|
44
|
+
.filter((field) =>
|
|
45
|
+
isTitleFieldInterface(
|
|
46
|
+
getFlowFieldInterfaceOptions(field.options?.interface || field.interface, dataSourceManager),
|
|
47
|
+
),
|
|
48
|
+
)
|
|
44
49
|
.map((field) => ({
|
|
45
50
|
value: field.name,
|
|
46
51
|
label: field?.title,
|
|
@@ -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,
|