@nocobase/client-v2 2.1.4 → 2.1.5
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/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +5 -0
- package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
- package/es/flow/utils/formValueDeps.d.ts +32 -0
- package/es/index.mjs +67 -67
- package/lib/index.js +81 -81
- package/package.json +7 -7
- package/src/components/AppComponents.tsx +19 -3
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
- package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
- package/src/flow/actions/linkageRules.tsx +1 -1
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +117 -18
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +185 -0
- package/src/flow/models/fields/InputFieldModel.tsx +48 -2
- package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +100 -1
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
- package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
- package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
- package/src/flow/utils/formValueDeps.ts +170 -0
|
@@ -13,7 +13,7 @@ import { subFormFieldLinkageRules } from '../linkageRules';
|
|
|
13
13
|
import { SubFormFieldModel, SubFormListFieldModel } from '../../models/fields/AssociationFieldModel/SubFormFieldModel';
|
|
14
14
|
|
|
15
15
|
describe('subFormFieldLinkageRules action', () => {
|
|
16
|
-
it('passes inputArgs to fork runtime context', async () => {
|
|
16
|
+
it('passes inputArgs to fork runtime context when grid forks are stored in a Set', async () => {
|
|
17
17
|
const linkageAssignHandler = vi.fn();
|
|
18
18
|
const engine = new FlowEngine();
|
|
19
19
|
const forkModel = new FlowModel({ uid: 'fork-model', flowEngine: engine }) as any;
|
|
@@ -38,7 +38,7 @@ describe('subFormFieldLinkageRules action', () => {
|
|
|
38
38
|
hidden: false,
|
|
39
39
|
subModels: {
|
|
40
40
|
grid: {
|
|
41
|
-
forks: [forkModel],
|
|
41
|
+
forks: new Set([forkModel]),
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
44
|
},
|
|
@@ -2842,7 +2842,7 @@ export const subFormFieldLinkageRules = defineAction({
|
|
|
2842
2842
|
}
|
|
2843
2843
|
} else {
|
|
2844
2844
|
await Promise.all(
|
|
2845
|
-
(grid.forks || []).map(async (forkModel: FlowModel) => {
|
|
2845
|
+
Array.from(grid.forks || []).map(async (forkModel: FlowModel) => {
|
|
2846
2846
|
if (forkModel.hidden) {
|
|
2847
2847
|
return;
|
|
2848
2848
|
}
|
|
@@ -15,17 +15,29 @@ import {
|
|
|
15
15
|
isRunJSValue,
|
|
16
16
|
} from '@nocobase/flow-engine';
|
|
17
17
|
import _ from 'lodash';
|
|
18
|
-
import { namePathToPathKey
|
|
18
|
+
import { namePathToPathKey } from '../models/blocks/form/value-runtime/path';
|
|
19
19
|
import {
|
|
20
20
|
collectStaticDepsFromRunJSValue,
|
|
21
21
|
collectStaticDepsFromTemplateValue,
|
|
22
22
|
recordDep,
|
|
23
23
|
type DepCollector,
|
|
24
24
|
} from '../models/blocks/form/value-runtime/deps';
|
|
25
|
+
import {
|
|
26
|
+
buildItemListRootPath,
|
|
27
|
+
buildItemRowPath,
|
|
28
|
+
dedupeNamePaths,
|
|
29
|
+
findFormValueChangeSource,
|
|
30
|
+
getChangedPathsFromPayload,
|
|
31
|
+
getFieldIndexEntriesFromContext,
|
|
32
|
+
isNamePathPrefix,
|
|
33
|
+
isSameNamePath,
|
|
34
|
+
minimizeNamePaths,
|
|
35
|
+
parseDependencyPath,
|
|
36
|
+
parsePathKey,
|
|
37
|
+
type NamePath,
|
|
38
|
+
} from '../utils/formValueDeps';
|
|
25
39
|
import { linkageRulesRefresh } from './linkageRulesRefresh';
|
|
26
40
|
|
|
27
|
-
type NamePath = Array<string | number>;
|
|
28
|
-
|
|
29
41
|
type LinkageRefreshDeps = {
|
|
30
42
|
wildcard: boolean;
|
|
31
43
|
valuePaths: NamePath[];
|
|
@@ -40,75 +52,11 @@ type LinkageRefreshBinding = {
|
|
|
40
52
|
dispose: () => void;
|
|
41
53
|
};
|
|
42
54
|
|
|
43
|
-
type FieldIndexEntry = {
|
|
44
|
-
name: string;
|
|
45
|
-
index: number;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
55
|
const FORM_VALUES_CHANGE_EVENT = 'formValuesChange';
|
|
49
56
|
const LINKAGE_REFRESH_BINDINGS_KEY = '__formValueDrivenLinkageRefreshBindings';
|
|
50
57
|
|
|
51
|
-
function isSameNamePath(a: NamePath, b: NamePath) {
|
|
52
|
-
return a.length === b.length && a.every((seg, index) => seg === b[index]);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function isNamePathPrefix(prefix: NamePath, path: NamePath) {
|
|
56
|
-
if (prefix.length > path.length) return false;
|
|
57
|
-
return prefix.every((seg, index) => seg === path[index]);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function dedupeNamePaths(paths: NamePath[]) {
|
|
61
|
-
const byKey = new Map<string, NamePath>();
|
|
62
|
-
for (const path of paths) {
|
|
63
|
-
if (!path?.length) continue;
|
|
64
|
-
byKey.set(namePathToPathKey(path), path);
|
|
65
|
-
}
|
|
66
|
-
return Array.from(byKey.values());
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function minimizeValueNamePaths(paths: NamePath[]) {
|
|
70
|
-
const deduped = dedupeNamePaths(paths);
|
|
71
|
-
return deduped.filter((path, index) => {
|
|
72
|
-
return !deduped.some((other, otherIndex) => otherIndex !== index && isNamePathPrefix(path, other));
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function parseFieldIndexEntries(fieldIndex: unknown): FieldIndexEntry[] {
|
|
77
|
-
const arr = Array.isArray(fieldIndex) ? fieldIndex : [];
|
|
78
|
-
const entries: FieldIndexEntry[] = [];
|
|
79
|
-
for (const it of arr) {
|
|
80
|
-
if (typeof it !== 'string') continue;
|
|
81
|
-
const [name, indexStr] = it.split(':');
|
|
82
|
-
const index = Number(indexStr);
|
|
83
|
-
if (!name || Number.isNaN(index)) continue;
|
|
84
|
-
entries.push({ name, index });
|
|
85
|
-
}
|
|
86
|
-
return entries;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function getFieldIndexEntriesFromContext(ctx: any): FieldIndexEntry[] {
|
|
90
|
-
return parseFieldIndexEntries(ctx?.model?.context?.fieldIndex ?? ctx?.fieldIndex);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function buildItemRowPath(entries: FieldIndexEntry[], parentDepth: number): NamePath | null {
|
|
94
|
-
const targetIndex = entries.length - 1 - parentDepth;
|
|
95
|
-
if (targetIndex < 0) return null;
|
|
96
|
-
|
|
97
|
-
const out: NamePath = [];
|
|
98
|
-
for (let i = 0; i <= targetIndex; i++) {
|
|
99
|
-
out.push(entries[i].name, entries[i].index);
|
|
100
|
-
}
|
|
101
|
-
return out;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function buildItemListRootPath(entries: FieldIndexEntry[], parentDepth: number): NamePath | null {
|
|
105
|
-
const rowPath = buildItemRowPath(entries, parentDepth);
|
|
106
|
-
if (!rowPath?.length) return null;
|
|
107
|
-
return rowPath.slice(0, -1);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
58
|
function resolveItemDependencyPath(ctx: FlowContext, depPath: NamePath): LinkageRefreshDeps {
|
|
111
|
-
const entries = getFieldIndexEntriesFromContext(ctx
|
|
59
|
+
const entries = getFieldIndexEntriesFromContext(ctx);
|
|
112
60
|
if (!entries.length) {
|
|
113
61
|
return { wildcard: true, valuePaths: [], structuralPaths: [] };
|
|
114
62
|
}
|
|
@@ -171,8 +119,7 @@ function addRunjsUsageToCollector(script: string, collector: DepCollector) {
|
|
|
171
119
|
collector.wildcard = true;
|
|
172
120
|
continue;
|
|
173
121
|
}
|
|
174
|
-
|
|
175
|
-
recordDep(segs, collector);
|
|
122
|
+
recordDep(parseDependencyPath(String(subPath)), collector);
|
|
176
123
|
continue;
|
|
177
124
|
}
|
|
178
125
|
if (varName === 'item') {
|
|
@@ -233,13 +180,13 @@ function collectLinkageRefreshDeps(ctx: FlowContext, params: any): LinkageRefres
|
|
|
233
180
|
wildcard = true;
|
|
234
181
|
continue;
|
|
235
182
|
}
|
|
236
|
-
valuePaths.push(
|
|
183
|
+
valuePaths.push(parsePathKey(inner));
|
|
237
184
|
continue;
|
|
238
185
|
}
|
|
239
186
|
|
|
240
187
|
if (depKey === 'ctx:item' || depKey.startsWith('ctx:item:')) {
|
|
241
188
|
const subPath = depKey === 'ctx:item' ? '' : depKey.slice('ctx:item:'.length);
|
|
242
|
-
const depPath = subPath ? (
|
|
189
|
+
const depPath = subPath ? parseDependencyPath(subPath) : [];
|
|
243
190
|
const resolved = resolveItemDependencyPath(ctx, depPath);
|
|
244
191
|
wildcard ||= resolved.wildcard;
|
|
245
192
|
valuePaths.push(...resolved.valuePaths);
|
|
@@ -249,7 +196,7 @@ function collectLinkageRefreshDeps(ctx: FlowContext, params: any): LinkageRefres
|
|
|
249
196
|
|
|
250
197
|
return {
|
|
251
198
|
wildcard,
|
|
252
|
-
valuePaths:
|
|
199
|
+
valuePaths: minimizeNamePaths(valuePaths),
|
|
253
200
|
structuralPaths: dedupeNamePaths(structuralPaths),
|
|
254
201
|
};
|
|
255
202
|
}
|
|
@@ -258,44 +205,9 @@ function hasLinkageRefreshDeps(deps: LinkageRefreshDeps) {
|
|
|
258
205
|
return deps.wildcard || deps.valuePaths.length > 0 || deps.structuralPaths.length > 0;
|
|
259
206
|
}
|
|
260
207
|
|
|
261
|
-
function getChangedPathsFromPayload(payload: any): NamePath[] {
|
|
262
|
-
const rawChangedPaths = Array.isArray(payload?.changedPaths) ? payload.changedPaths : [];
|
|
263
|
-
const out: NamePath[] = [];
|
|
264
|
-
|
|
265
|
-
for (const path of rawChangedPaths) {
|
|
266
|
-
if (Array.isArray(path)) {
|
|
267
|
-
if (path.length === 1 && typeof path[0] === 'string') {
|
|
268
|
-
const namePath = pathKeyToNamePath(path[0]);
|
|
269
|
-
if (namePath.length) out.push(namePath);
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
const segs = path.filter((seg) => typeof seg === 'string' || typeof seg === 'number') as NamePath;
|
|
273
|
-
if (segs.length) out.push(segs);
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
if (typeof path === 'string' && path) {
|
|
277
|
-
out.push(pathKeyToNamePath(path));
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (out.length) {
|
|
282
|
-
return out;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const changedValues = payload?.changedValues;
|
|
286
|
-
if (changedValues && typeof changedValues === 'object') {
|
|
287
|
-
for (const key of Object.keys(changedValues)) {
|
|
288
|
-
const namePath = pathKeyToNamePath(key);
|
|
289
|
-
if (namePath.length) out.push(namePath);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return out;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
208
|
function linkageRefreshDepsMatchPayload(deps: LinkageRefreshDeps, payload: any) {
|
|
297
209
|
if (!hasLinkageRefreshDeps(deps)) return false;
|
|
298
|
-
const changedPaths = getChangedPathsFromPayload(payload);
|
|
210
|
+
const changedPaths = getChangedPathsFromPayload(payload, { includeArrayChangedValues: true });
|
|
299
211
|
if (deps.wildcard) return true;
|
|
300
212
|
if (!changedPaths.length) return true;
|
|
301
213
|
|
|
@@ -330,28 +242,8 @@ function getLinkageRefreshBindings(model: any): Map<string, LinkageRefreshBindin
|
|
|
330
242
|
return (model[LINKAGE_REFRESH_BINDINGS_KEY] ||= new Map<string, LinkageRefreshBinding>());
|
|
331
243
|
}
|
|
332
244
|
|
|
333
|
-
function isFormBlockForLinkageRefresh(model: any) {
|
|
334
|
-
if (!model || typeof model !== 'object') return false;
|
|
335
|
-
if (!model.emitter || typeof model.emitter.on !== 'function' || typeof model.emitter.off !== 'function') return false;
|
|
336
|
-
return !!model.formValueRuntime || !!model.context?.form || typeof model.context?.setFormValues === 'function';
|
|
337
|
-
}
|
|
338
|
-
|
|
339
245
|
function findFormBlockForLinkageRefresh(ctx: FlowContext): any | null {
|
|
340
|
-
|
|
341
|
-
const push = (model: any) => {
|
|
342
|
-
if (model && !candidates.includes(model)) candidates.push(model);
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
push((ctx.model as any)?.context?.blockModel);
|
|
346
|
-
push(ctx.model);
|
|
347
|
-
|
|
348
|
-
let cursor: any = (ctx.model as any)?.parent;
|
|
349
|
-
while (cursor) {
|
|
350
|
-
push(cursor);
|
|
351
|
-
cursor = cursor?.parent;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return candidates.find(isFormBlockForLinkageRefresh) || null;
|
|
246
|
+
return findFormValueChangeSource(ctx) as any;
|
|
355
247
|
}
|
|
356
248
|
|
|
357
249
|
function disposeLinkageRefreshBinding(model: any, key: string) {
|
package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
observer,
|
|
27
27
|
FlowModelProvider,
|
|
28
28
|
FlowErrorFallback,
|
|
29
|
+
extractUsedVariablePaths,
|
|
29
30
|
} from '@nocobase/flow-engine';
|
|
30
31
|
import { TableColumnProps, Tooltip, Input, Space, Divider } from 'antd';
|
|
31
32
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
@@ -37,6 +38,14 @@ import { FieldDeletePlaceholder, CustomWidth, normalizeTableColumnWidth } from '
|
|
|
37
38
|
import { buildDynamicNamePath } from '../../../blocks/form/dynamicNamePath';
|
|
38
39
|
import { getSubTableRowIdentity } from './rowIdentity';
|
|
39
40
|
import { getFieldBindingUse, rebuildFieldSubModel } from '../../../../internal/utils/rebuildFieldSubModel';
|
|
41
|
+
import {
|
|
42
|
+
buildCurrentItemTitle,
|
|
43
|
+
createAssociationItemChainContextPropertyOptions,
|
|
44
|
+
createItemChainGetter,
|
|
45
|
+
createParentItemAccessorsFromContext,
|
|
46
|
+
createRootItemChain,
|
|
47
|
+
type ItemChain,
|
|
48
|
+
} from '../itemChain';
|
|
40
49
|
|
|
41
50
|
export const SUB_TABLE_COLUMN_FIELD_COMPONENT_CONTEXT = 'subTableColumn';
|
|
42
51
|
|
|
@@ -180,6 +189,25 @@ const handleModelName = (modelName) => {
|
|
|
180
189
|
return modelName;
|
|
181
190
|
};
|
|
182
191
|
|
|
192
|
+
const DATA_SCOPE_FLOW_KEYS = ['selectSettings', 'cascadeSelectSettings'];
|
|
193
|
+
|
|
194
|
+
function hasFormValueDrivenDataScope(filter: any) {
|
|
195
|
+
if (!filter) return false;
|
|
196
|
+
try {
|
|
197
|
+
const usage = extractUsedVariablePaths(filter) || {};
|
|
198
|
+
return ['item', 'formValues'].some((name) => Object.prototype.hasOwnProperty.call(usage, name));
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function hasFormValueDrivenDataScopeField(fieldModel: any) {
|
|
205
|
+
return DATA_SCOPE_FLOW_KEYS.some((flowKey) => {
|
|
206
|
+
const params = fieldModel?.getStepParams?.(flowKey, 'dataScope');
|
|
207
|
+
return hasFormValueDrivenDataScope(params?.filter);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
183
211
|
const MemoFieldRenderer = React.memo(FieldModelRenderer, (prev, next) => {
|
|
184
212
|
return prev.value === next.value && prev.model === next.model;
|
|
185
213
|
});
|
|
@@ -348,7 +376,10 @@ const MemoCell: React.FC<CellProps> = React.memo(
|
|
|
348
376
|
const fork: any = action.createFork({}, `${id}`);
|
|
349
377
|
fork.context.defineProperty('currentObject', { get: () => record });
|
|
350
378
|
if (rowFork) {
|
|
379
|
+
const itemOptions = rowFork.context.getPropertyOptions?.('item');
|
|
380
|
+
const { value: _value, ...itemOptionsWithoutValue } = (itemOptions || {}) as any;
|
|
351
381
|
fork.context.defineProperty('item', {
|
|
382
|
+
...itemOptionsWithoutValue,
|
|
352
383
|
get: () => rowFork.context.item,
|
|
353
384
|
cache: false,
|
|
354
385
|
});
|
|
@@ -445,6 +476,57 @@ export class SubTableColumnModel<
|
|
|
445
476
|
static renderMode = ModelRenderMode.RenderFunction;
|
|
446
477
|
static fieldComponentContext = SUB_TABLE_COLUMN_FIELD_COMPONENT_CONTEXT;
|
|
447
478
|
|
|
479
|
+
private getAssociationField() {
|
|
480
|
+
return (this.parent as any)?.context?.collectionField;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private getParentFormValues() {
|
|
484
|
+
return (this.context?.blockModel as any)?.context?.formValues;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private createParentItemAccessors(parentItemAccessor?: () => ItemChain | undefined) {
|
|
488
|
+
const baseAccessors = createParentItemAccessorsFromContext({
|
|
489
|
+
parentContextAccessor: () => (this.parent as any)?.context,
|
|
490
|
+
fallbackParentPropertiesAccessor: () => this.getParentFormValues(),
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
if (!parentItemAccessor) {
|
|
494
|
+
return baseAccessors;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
parentPropertiesAccessor: (ctx?: any) =>
|
|
499
|
+
parentItemAccessor()?.value ?? baseAccessors.parentPropertiesAccessor(ctx),
|
|
500
|
+
parentItemMetaAccessor: baseAccessors.parentItemMetaAccessor,
|
|
501
|
+
parentItemResolverAccessor: baseAccessors.parentItemResolverAccessor,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private createRowItemContextPropertyOptions(
|
|
506
|
+
options: {
|
|
507
|
+
resolverPropertiesAccessor?: () => unknown;
|
|
508
|
+
parentItemAccessor?: () => ItemChain | undefined;
|
|
509
|
+
showParentIndex?: boolean;
|
|
510
|
+
} = {},
|
|
511
|
+
) {
|
|
512
|
+
const associationField = this.getAssociationField();
|
|
513
|
+
|
|
514
|
+
return createAssociationItemChainContextPropertyOptions({
|
|
515
|
+
t: this.context.t,
|
|
516
|
+
title: buildCurrentItemTitle(this.context.t, associationField, this.props?.name),
|
|
517
|
+
showIndex: true,
|
|
518
|
+
showParentIndex:
|
|
519
|
+
options.showParentIndex ??
|
|
520
|
+
(Array.isArray((this.parent as any)?.context?.fieldIndex) &&
|
|
521
|
+
(this.parent as any).context.fieldIndex.length > 0),
|
|
522
|
+
collectionAccessor: () => (this.parent as any)?.collection ?? associationField?.targetCollection ?? null,
|
|
523
|
+
propertiesAccessor: (ctx) => ctx?.item?.value,
|
|
524
|
+
resolverPropertiesAccessor: options.resolverPropertiesAccessor ?? (() => undefined),
|
|
525
|
+
parentCollectionAccessor: () => associationField?.collection,
|
|
526
|
+
parentAccessors: this.createParentItemAccessors(options.parentItemAccessor),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
448
530
|
renderHiddenInConfig() {
|
|
449
531
|
return <FieldWithoutPermissionPlaceholder targetModel={this} />;
|
|
450
532
|
}
|
|
@@ -505,6 +587,14 @@ export class SubTableColumnModel<
|
|
|
505
587
|
).some(Boolean);
|
|
506
588
|
}
|
|
507
589
|
|
|
590
|
+
get hasFormValueDrivenDataScopeColumn() {
|
|
591
|
+
return (
|
|
592
|
+
this.parent?.mapSubModels('columns', (column: SubTableColumnModel) => {
|
|
593
|
+
return hasFormValueDrivenDataScopeField(column?.subModels?.field);
|
|
594
|
+
}) || []
|
|
595
|
+
).some(Boolean);
|
|
596
|
+
}
|
|
597
|
+
|
|
508
598
|
onInit(options: any): void {
|
|
509
599
|
super.onInit(options);
|
|
510
600
|
this.context.defineProperty('resourceName', {
|
|
@@ -516,6 +606,10 @@ export class SubTableColumnModel<
|
|
|
516
606
|
this.context.defineProperty('actionName', {
|
|
517
607
|
get: () => 'view',
|
|
518
608
|
});
|
|
609
|
+
this.context.defineProperty('item', {
|
|
610
|
+
get: () => undefined,
|
|
611
|
+
...this.createRowItemContextPropertyOptions(),
|
|
612
|
+
});
|
|
519
613
|
this.emitter.on('onSubModelAdded', (subModel: FieldModel) => {
|
|
520
614
|
if (this.collectionField) {
|
|
521
615
|
subModel.setProps(this.collectionField.getComponentProps());
|
|
@@ -673,25 +767,30 @@ export class SubTableColumnModel<
|
|
|
673
767
|
value: [...baseArr, `${associationKey}:${rowIndex}`],
|
|
674
768
|
});
|
|
675
769
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
770
|
+
const getRowRecord = () => {
|
|
771
|
+
const form = (fork.context as any)?.form || (this.context?.blockModel as any)?.context?.form;
|
|
772
|
+
return getLatestSubTableRowRecord(form, fork.context.fieldIndex, record);
|
|
773
|
+
};
|
|
774
|
+
const getParentItem = () => {
|
|
775
|
+
const parentItemCtx = (parentItem ?? (this.parent as any)?.context?.item) as ItemChain | undefined;
|
|
776
|
+
return parentItemCtx ?? createRootItemChain(this.getParentFormValues());
|
|
777
|
+
};
|
|
778
|
+
const getRowItem = createItemChainGetter({
|
|
779
|
+
valueAccessor: getRowRecord,
|
|
780
|
+
parentItemAccessor: getParentItem,
|
|
781
|
+
indexAccessor: () => (Number.isFinite(rowIndex) ? rowIndex : undefined),
|
|
782
|
+
lengthAccessor: () => {
|
|
683
783
|
const list = (this.parent as any)?.props?.value;
|
|
684
|
-
|
|
685
|
-
return {
|
|
686
|
-
index: Number.isFinite(rowIndex) ? rowIndex : undefined,
|
|
687
|
-
length,
|
|
688
|
-
__is_new__: isNew,
|
|
689
|
-
__is_stored__: isStored,
|
|
690
|
-
value: rowRecord,
|
|
691
|
-
parentItem: parentItemCtx,
|
|
692
|
-
};
|
|
784
|
+
return Array.isArray(list) ? list.length : undefined;
|
|
693
785
|
},
|
|
694
|
-
|
|
786
|
+
});
|
|
787
|
+
fork.context.defineProperty('item', {
|
|
788
|
+
get: getRowItem,
|
|
789
|
+
...this.createRowItemContextPropertyOptions({
|
|
790
|
+
resolverPropertiesAccessor: getRowRecord,
|
|
791
|
+
parentItemAccessor: getParentItem,
|
|
792
|
+
showParentIndex: baseArr.length > 0,
|
|
793
|
+
}),
|
|
695
794
|
});
|
|
696
795
|
return fork;
|
|
697
796
|
})();
|
|
@@ -707,7 +806,7 @@ export class SubTableColumnModel<
|
|
|
707
806
|
rowFork={rowFork}
|
|
708
807
|
memoKey={cellModeKey}
|
|
709
808
|
width={this.props.width}
|
|
710
|
-
commitOnChange={this.hasFormulaColumn}
|
|
809
|
+
commitOnChange={this.hasFormulaColumn || this.hasFormValueDrivenDataScopeColumn}
|
|
711
810
|
/>
|
|
712
811
|
);
|
|
713
812
|
};
|
|
@@ -21,6 +21,26 @@ import {
|
|
|
21
21
|
isSubTableColumnReadPretty,
|
|
22
22
|
} from '../SubTableColumnModel';
|
|
23
23
|
|
|
24
|
+
function createMockCollection(name: string, fields: any[] = []) {
|
|
25
|
+
const collection: any = {
|
|
26
|
+
name,
|
|
27
|
+
title: name,
|
|
28
|
+
dataSourceKey: 'main',
|
|
29
|
+
filterTargetKey: 'id',
|
|
30
|
+
fields,
|
|
31
|
+
getFields: () => collection.fields,
|
|
32
|
+
getField: (fieldName: string) => collection.fields.find((field) => field.name === fieldName),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
collection.fields.forEach((field) => {
|
|
36
|
+
field.collection = field.collection ?? collection;
|
|
37
|
+
field.filterable = field.filterable ?? true;
|
|
38
|
+
field.isAssociationField = field.isAssociationField ?? (() => false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return collection;
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
describe('SubTableColumnModel row record helpers', () => {
|
|
25
45
|
it('builds the row path from fieldIndex entries', () => {
|
|
26
46
|
expect(buildRowPathFromFieldIndex(['roles:0'])).toEqual(['roles', 0]);
|
|
@@ -51,6 +71,171 @@ describe('SubTableColumnModel row record helpers', () => {
|
|
|
51
71
|
expect(getLatestSubTableRowRecord(form, ['roles:0'], fallback)).toBe(fallback);
|
|
52
72
|
});
|
|
53
73
|
|
|
74
|
+
it('provides current item meta for sub-table column data scope variables', async () => {
|
|
75
|
+
const engine = new FlowEngine();
|
|
76
|
+
engine.registerModels({ SubTableColumnModel });
|
|
77
|
+
|
|
78
|
+
const tagsCollection = createMockCollection('tags', [
|
|
79
|
+
{ name: 'id', title: 'ID', type: 'integer', interface: 'integer' },
|
|
80
|
+
{ name: 'name', title: 'Name', type: 'string', interface: 'input' },
|
|
81
|
+
]);
|
|
82
|
+
const rolesCollection = createMockCollection('roles', [
|
|
83
|
+
{ name: 'id', title: 'ID', type: 'integer', interface: 'integer' },
|
|
84
|
+
{ name: 'name', title: 'Name', type: 'string', interface: 'input' },
|
|
85
|
+
{
|
|
86
|
+
name: 'tags',
|
|
87
|
+
title: 'Tags',
|
|
88
|
+
type: 'hasMany',
|
|
89
|
+
interface: 'o2m',
|
|
90
|
+
target: 'tags',
|
|
91
|
+
targetCollection: tagsCollection,
|
|
92
|
+
isAssociationField: () => true,
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
const usersCollection = createMockCollection('users', [
|
|
96
|
+
{ name: 'id', title: 'ID', type: 'integer', interface: 'integer' },
|
|
97
|
+
{ name: 'nickname', title: 'Nickname', type: 'string', interface: 'input' },
|
|
98
|
+
]);
|
|
99
|
+
const rolesField = {
|
|
100
|
+
name: 'roles',
|
|
101
|
+
title: 'Roles',
|
|
102
|
+
type: 'hasMany',
|
|
103
|
+
interface: 'o2m',
|
|
104
|
+
collection: usersCollection,
|
|
105
|
+
target: 'roles',
|
|
106
|
+
targetCollection: rolesCollection,
|
|
107
|
+
isAssociationField: () => true,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const form = {
|
|
111
|
+
getFieldValue: vi.fn((path: any) => {
|
|
112
|
+
if (JSON.stringify(path) === JSON.stringify(['roles', 0])) {
|
|
113
|
+
return { id: 11, name: 'Admin', tags: [{ id: 7 }] };
|
|
114
|
+
}
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
const blockModel: any = engine.createModel({ use: 'FlowModel', uid: 'form-block', structure: {} as any });
|
|
118
|
+
blockModel.context.defineProperty('form', { value: form });
|
|
119
|
+
blockModel.context.defineProperty('formValues', {
|
|
120
|
+
value: {
|
|
121
|
+
id: 1,
|
|
122
|
+
nickname: 'Alice',
|
|
123
|
+
roles: [{ id: 11, name: 'Admin' }],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const subTableFieldModel: any = engine.createModel({
|
|
128
|
+
use: 'FlowModel',
|
|
129
|
+
uid: 'users.roles',
|
|
130
|
+
structure: {} as any,
|
|
131
|
+
});
|
|
132
|
+
subTableFieldModel.collection = rolesCollection;
|
|
133
|
+
subTableFieldModel.setProps({ value: [{ id: 11, name: 'Admin' }] });
|
|
134
|
+
subTableFieldModel.context.defineProperty('collectionField', { value: rolesField });
|
|
135
|
+
subTableFieldModel.context.defineProperty('fieldPath', { value: 'roles' });
|
|
136
|
+
subTableFieldModel.context.defineProperty('blockModel', { value: blockModel });
|
|
137
|
+
|
|
138
|
+
const column: any = engine.createModel({
|
|
139
|
+
use: 'SubTableColumnModel',
|
|
140
|
+
uid: 'users.roles.tags',
|
|
141
|
+
stepParams: {
|
|
142
|
+
fieldSettings: {
|
|
143
|
+
init: {
|
|
144
|
+
fieldPath: 'roles.tags',
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
structure: {} as any,
|
|
149
|
+
});
|
|
150
|
+
column.setParent(subTableFieldModel);
|
|
151
|
+
column.context.defineProperty('blockModel', { value: blockModel });
|
|
152
|
+
column.subModels = { field: [] };
|
|
153
|
+
|
|
154
|
+
const designTimeItemOptions = column.context.getPropertyOptions('item');
|
|
155
|
+
expect(typeof designTimeItemOptions.meta).toBe('function');
|
|
156
|
+
const designTimeMeta = await designTimeItemOptions.meta();
|
|
157
|
+
expect(designTimeMeta.properties.value.title).toBe('Attributes');
|
|
158
|
+
expect(designTimeMeta.properties.parentItem.properties.value.title).toBe('Attributes');
|
|
159
|
+
|
|
160
|
+
const renderCell = column.renderItem();
|
|
161
|
+
renderCell({
|
|
162
|
+
id: 'cell-roles-tags-0',
|
|
163
|
+
rowIdx: 0,
|
|
164
|
+
record: { id: 11, name: 'Stale admin', tags: [{ id: 7 }] },
|
|
165
|
+
parentItem: {
|
|
166
|
+
value: { id: 1, nickname: 'Alice' },
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const [rowFork] = Array.from(column.forks ?? []) as any[];
|
|
171
|
+
const rowItemOptions = rowFork.context.getPropertyOptions('item');
|
|
172
|
+
expect(typeof rowItemOptions.meta).toBe('function');
|
|
173
|
+
expect(rowItemOptions.resolveOnServer('value.tags.name')).toBe(true);
|
|
174
|
+
|
|
175
|
+
const rowMeta = await rowItemOptions.meta();
|
|
176
|
+
expect(rowMeta.properties.value.title).toBe('Attributes');
|
|
177
|
+
expect(rowMeta.properties.parentItem.properties.value.title).toBe('Attributes');
|
|
178
|
+
expect(rowFork.context.item).toMatchObject({
|
|
179
|
+
index: 0,
|
|
180
|
+
length: 1,
|
|
181
|
+
value: { id: 11, name: 'Admin', tags: [{ id: 7 }] },
|
|
182
|
+
parentItem: {
|
|
183
|
+
value: { id: 1, nickname: 'Alice' },
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('detects variable data scope columns for immediate row commits', () => {
|
|
189
|
+
const engine = new FlowEngine();
|
|
190
|
+
engine.registerModels({ SubTableColumnModel });
|
|
191
|
+
|
|
192
|
+
const inputColumn: any = engine.createModel({
|
|
193
|
+
use: 'SubTableColumnModel',
|
|
194
|
+
uid: 'users.roles.name',
|
|
195
|
+
structure: {} as any,
|
|
196
|
+
});
|
|
197
|
+
const scopedColumn: any = engine.createModel({
|
|
198
|
+
use: 'SubTableColumnModel',
|
|
199
|
+
uid: 'users.roles.tags',
|
|
200
|
+
structure: {} as any,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
inputColumn.subModels = {
|
|
204
|
+
field: {
|
|
205
|
+
getStepParams: vi.fn(() => undefined),
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
scopedColumn.subModels = {
|
|
209
|
+
field: {
|
|
210
|
+
getStepParams: vi.fn((flowKey: string, stepKey: string) => {
|
|
211
|
+
if (flowKey !== 'selectSettings' || stepKey !== 'dataScope') {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
filter: {
|
|
216
|
+
logic: '$and',
|
|
217
|
+
items: [{ path: 'name', operator: '$eq', value: '{{ ctx.item.value.name }}' }],
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const parent: any = engine.createModel({
|
|
225
|
+
use: 'FlowModel',
|
|
226
|
+
uid: 'users.roles-table',
|
|
227
|
+
structure: {} as any,
|
|
228
|
+
});
|
|
229
|
+
parent.mapSubModels = vi.fn((key: string, iterator: (column: SubTableColumnModel) => unknown) => {
|
|
230
|
+
if (key !== 'columns') return [];
|
|
231
|
+
return [inputColumn, scopedColumn].map(iterator);
|
|
232
|
+
});
|
|
233
|
+
inputColumn.setParent(parent);
|
|
234
|
+
scopedColumn.setParent(parent);
|
|
235
|
+
|
|
236
|
+
expect(inputColumn.hasFormValueDrivenDataScopeColumn).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
54
239
|
it('treats a display-only column pattern as read-pretty mode', () => {
|
|
55
240
|
expect(isSubTableColumnReadPretty({ props: { pattern: 'readPretty' } })).toBe(true);
|
|
56
241
|
expect(isSubTableColumnReadPretty({ props: { readPretty: true } })).toBe(true);
|