@nocobase/client-v2 2.1.0-alpha.30 → 2.1.0-alpha.31
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/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/blocks/filter-form/FilterFormGridModel.d.ts +15 -6
- package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.d.ts +9 -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 +90 -90
- package/lib/index.js +87 -87
- package/package.json +5 -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/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +438 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +42 -0
- package/src/flow/actions/linkageRules.tsx +8 -1
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +492 -0
- package/src/flow/actions/linkageRulesRefresh.tsx +4 -2
- package/src/flow/actions/titleField.tsx +8 -3
- 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/__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/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 +21 -5
- package/src/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.ts +20 -0
- package/src/flow/models/fields/DividerItemModel.tsx +30 -15
- 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
|
|
|
@@ -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,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
11
|
-
import { lazy } from '../../../flow-compat';
|
|
11
|
+
import { getFlowFieldInterfaceOptions, lazy } from '../../../flow-compat';
|
|
12
12
|
import { Input, InputNumber, Select, Space, Switch } from 'antd';
|
|
13
13
|
import merge from 'lodash/merge';
|
|
14
14
|
import uniqBy from 'lodash/uniqBy';
|
|
@@ -227,11 +227,12 @@ export const LinkageFilterItem: React.FC<LinkageFilterItemProps> = observer((pro
|
|
|
227
227
|
|
|
228
228
|
const operatorMetadataList: OperatorMeta[] = useMemo(() => {
|
|
229
229
|
if (leftFieldMeta) {
|
|
230
|
-
const dataSourceManager = model.context.app.dataSourceManager;
|
|
231
230
|
const fieldInterface = leftFieldMeta.interface
|
|
232
|
-
? (
|
|
231
|
+
? (getFlowFieldInterfaceOptions(
|
|
233
232
|
leftFieldMeta.interface,
|
|
234
|
-
|
|
233
|
+
model.context.dataSourceManager,
|
|
234
|
+
model.context.app?.dataSourceManager,
|
|
235
|
+
) as FieldInterfaceDef | undefined)
|
|
235
236
|
: undefined;
|
|
236
237
|
const schemaOperators = (leftFieldMeta as any)?.uiSchema?.['x-filter-operators'] as
|
|
237
238
|
| Array<OperatorMeta & { visible?: (meta: MetaTreeNode) => boolean }>
|
|
@@ -392,7 +393,7 @@ export const LinkageFilterItem: React.FC<LinkageFilterItemProps> = observer((pro
|
|
|
392
393
|
const base = Array.isArray(tree) ? tree : [];
|
|
393
394
|
const merged = mergeExtraMetaTreeWithBase(base, extraMetaTree);
|
|
394
395
|
const getFieldInterface = (name: string) =>
|
|
395
|
-
model.context.app?.dataSourceManager
|
|
396
|
+
getFlowFieldInterfaceOptions(name, model.context.dataSourceManager, model.context.app?.dataSourceManager) as
|
|
396
397
|
| FieldInterfaceDef
|
|
397
398
|
| undefined;
|
|
398
399
|
return await enhanceMetaTreeWithFilterableChildren(merged, getFieldInterface);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
|
11
11
|
import { Cascader, Checkbox, Input, InputNumber, Radio, Select, Space, Switch } from 'antd';
|
|
12
|
-
import { lazy } from '../../../flow-compat';
|
|
12
|
+
import { getFlowFieldInterfaceOptions, lazy } from '../../../flow-compat';
|
|
13
13
|
import merge from 'lodash/merge';
|
|
14
14
|
import type { ISchema } from '@formily/json-schema';
|
|
15
15
|
import {
|
|
@@ -286,18 +286,24 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
286
286
|
children?: Array<{ name: string; title?: string; schema?: ISchema; operators?: OperatorMeta[] }>;
|
|
287
287
|
};
|
|
288
288
|
};
|
|
289
|
+
const getFieldInterface = useCallback(
|
|
290
|
+
(interfaceName: string | undefined) =>
|
|
291
|
+
getFlowFieldInterfaceOptions(
|
|
292
|
+
interfaceName,
|
|
293
|
+
model.context.dataSourceManager,
|
|
294
|
+
model.context.app?.dataSourceManager,
|
|
295
|
+
) as FieldInterfaceDef | undefined,
|
|
296
|
+
[model],
|
|
297
|
+
);
|
|
289
298
|
|
|
290
299
|
// 基于字段接口的动态操作符元数据(优先使用子菜单 schema 中自定义的 operators,其次再用接口默认 operators)
|
|
291
300
|
const operatorMetaList: OperatorMeta[] = useMemo(() => {
|
|
292
301
|
if (!leftMeta) return [];
|
|
293
|
-
const
|
|
294
|
-
const fi = leftMeta.interface
|
|
295
|
-
? (dm?.collectionFieldInterfaceManager?.getFieldInterface(leftMeta.interface) as FieldInterfaceDef | undefined)
|
|
296
|
-
: undefined;
|
|
302
|
+
const fi = leftMeta.interface ? getFieldInterface(leftMeta.interface) : undefined;
|
|
297
303
|
const schemaOps: OperatorMeta[] | undefined = (leftMeta as any)?.uiSchema?.['x-filter-operators'];
|
|
298
304
|
const baseOps = (Array.isArray(schemaOps) && schemaOps.length ? schemaOps : fi?.filterable?.operators) || [];
|
|
299
305
|
return baseOps.filter((op) => !op.visible || op.visible(leftMeta));
|
|
300
|
-
}, [
|
|
306
|
+
}, [getFieldInterface, leftMeta]);
|
|
301
307
|
|
|
302
308
|
useEffect(() => {
|
|
303
309
|
if (!operatorMetaList.length) return;
|
|
@@ -578,16 +584,11 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
578
584
|
const enhancedMetaTree = useMemo(() => {
|
|
579
585
|
type MetaTreeProvider = () => MetaTreeNode[] | Promise<MetaTreeNode[]>;
|
|
580
586
|
return async () => {
|
|
581
|
-
const dm = model.context.app?.dataSourceManager;
|
|
582
|
-
const fiMgr = dm?.collectionFieldInterfaceManager;
|
|
583
|
-
|
|
584
587
|
// 优先复用已注入 meta;否则在本组件范围内临时构建
|
|
585
588
|
const nodes: MetaTreeNode[] = await buildCollectionLeftMetaTreeLocal(model.context);
|
|
586
589
|
|
|
587
590
|
const enhanceNode = async (node: MetaTreeNode): Promise<MetaTreeNode> => {
|
|
588
|
-
const fi = node.interface
|
|
589
|
-
? (fiMgr?.getFieldInterface(node.interface) as FieldInterfaceDef | undefined)
|
|
590
|
-
: undefined;
|
|
591
|
+
const fi = node.interface ? getFieldInterface(node.interface) : undefined;
|
|
591
592
|
const extraChildren: MetaTreeNode[] = [];
|
|
592
593
|
const filterable = fi?.filterable;
|
|
593
594
|
const childrenDefs = filterable?.children as
|
|
@@ -634,7 +635,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
634
635
|
}
|
|
635
636
|
return out;
|
|
636
637
|
};
|
|
637
|
-
}, [model]);
|
|
638
|
+
}, [getFieldInterface, model]);
|
|
638
639
|
|
|
639
640
|
return (
|
|
640
641
|
<Space wrap style={{ width: '100%' }}>
|
|
@@ -110,6 +110,39 @@ describe('LinkageFilterItem', () => {
|
|
|
110
110
|
expect(screen.queryByText('[object Object]')).toBeNull();
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
it('uses scoped context dataSourceManager when app dataSourceManager has no field interface manager', async () => {
|
|
114
|
+
const value = observable({ path: '', operator: '', value: '' }) as any;
|
|
115
|
+
const { model, app } = createModel();
|
|
116
|
+
const getRuntimeFieldInterface = vi.fn((name: string) => ({
|
|
117
|
+
name,
|
|
118
|
+
filterable: {
|
|
119
|
+
operators: [{ value: '$eq', label: 'Equals', selected: true }],
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
model.context.dataSourceManager.setCollectionFieldInterfaceManager({
|
|
123
|
+
getFieldInterface: getRuntimeFieldInterface,
|
|
124
|
+
});
|
|
125
|
+
(app as any).dataSourceManager = {};
|
|
126
|
+
|
|
127
|
+
(globalThis as any).__TEST_PATH__ = 'assignee';
|
|
128
|
+
(globalThis as any).__TEST_META__ = {
|
|
129
|
+
interface: 'belongsTo',
|
|
130
|
+
uiSchema: { 'x-component': 'RecordPicker' },
|
|
131
|
+
paths: ['collection', 'assignee'],
|
|
132
|
+
name: 'assignee',
|
|
133
|
+
title: 'Assignee',
|
|
134
|
+
type: 'object',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
render(<LinkageFilterItem value={value} model={model} />);
|
|
138
|
+
fireEvent.click(screen.getByTestId('variable-input'));
|
|
139
|
+
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
expect(value.operator).toBe('$eq');
|
|
142
|
+
expect(getRuntimeFieldInterface).toHaveBeenCalledWith('belongsTo');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
113
146
|
it('renders operator schema component for multi-keyword constants', async () => {
|
|
114
147
|
const value = observable({ path: '', operator: '', value: 'foo\nbar' }) as any;
|
|
115
148
|
const { model, app } = createModel();
|