@nocobase/client-v2 2.1.36 → 2.1.38
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/linkageRules.d.ts +24 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +2 -23
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +23 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +28 -0
- package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
- package/es/flow/components/field-value-variable/index.d.ts +11 -0
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +6 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/index.mjs +65 -74
- package/lib/index.js +92 -101
- package/package.json +7 -7
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/linkageRules.tsx +25 -7
- package/src/flow/components/FieldAssignValueInput.tsx +34 -571
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +162 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +306 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
- package/src/flow/components/field-value-variable/dateValue.ts +223 -0
- package/src/flow/components/field-value-variable/index.ts +12 -0
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- package/src/flow/models/blocks/form/FormBlockModel.tsx +4 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +68 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +46 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +1 -0
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -2
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { isObservable, reaction, toJS } from '@formily/reactive';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
FlowContext,
|
|
13
|
+
FlowModel,
|
|
14
|
+
isRunJSValue,
|
|
15
|
+
normalizeRunJSValue,
|
|
16
|
+
runjsWithSafeGlobals,
|
|
17
|
+
type ResolveJsonTemplateOptions,
|
|
18
|
+
} from '@nocobase/flow-engine';
|
|
12
19
|
import { getValuesByPath } from '@nocobase/shared';
|
|
13
20
|
import _ from 'lodash';
|
|
14
21
|
import { dayjs } from '@nocobase/utils/client';
|
|
@@ -49,6 +56,7 @@ type RuntimeRule = {
|
|
|
49
56
|
getValue: () => any;
|
|
50
57
|
getCondition?: () => any;
|
|
51
58
|
getContext: () => any;
|
|
59
|
+
getContractModelUid?: () => string | undefined;
|
|
52
60
|
};
|
|
53
61
|
|
|
54
62
|
type ObservableBinding = {
|
|
@@ -83,8 +91,18 @@ type ToManyAggregateSourceInfo = {
|
|
|
83
91
|
lastWrite?: FormValueWriteMeta;
|
|
84
92
|
};
|
|
85
93
|
|
|
94
|
+
type RuntimeItemChain = {
|
|
95
|
+
index?: number;
|
|
96
|
+
length?: number;
|
|
97
|
+
__is_new__?: boolean;
|
|
98
|
+
__is_stored__?: boolean;
|
|
99
|
+
value: unknown;
|
|
100
|
+
parentItem?: RuntimeItemChain;
|
|
101
|
+
};
|
|
102
|
+
|
|
86
103
|
export type RuleEngineOptions = {
|
|
87
104
|
getBlockModelUid: () => string;
|
|
105
|
+
getAssignRulesModelUid: () => string | undefined;
|
|
88
106
|
getActionName: () => string | undefined;
|
|
89
107
|
getBlockContext: () => any;
|
|
90
108
|
getEngine: () => any;
|
|
@@ -208,6 +226,7 @@ export class RuleEngine {
|
|
|
208
226
|
getValue: () => template?.value,
|
|
209
227
|
getCondition: () => template?.condition,
|
|
210
228
|
getContext: () => this.options.getBlockContext(),
|
|
229
|
+
getContractModelUid: this.options.getAssignRulesModelUid,
|
|
211
230
|
};
|
|
212
231
|
|
|
213
232
|
this.rules.set(id, { rule, state: { deps: new Set(), depDisposers: [], runSeq: 0, scheduledAtWriteSeq: 0 } });
|
|
@@ -545,6 +564,7 @@ export class RuleEngine {
|
|
|
545
564
|
getValue: () => template?.value,
|
|
546
565
|
getCondition: () => template?.condition,
|
|
547
566
|
getContext: () => this.options.getBlockContext(),
|
|
567
|
+
getContractModelUid: this.options.getAssignRulesModelUid,
|
|
548
568
|
};
|
|
549
569
|
|
|
550
570
|
this.rules.set(id, { rule, state: { deps: new Set(), depDisposers: [], runSeq: 0, scheduledAtWriteSeq: 0 } });
|
|
@@ -913,6 +933,7 @@ export class RuleEngine {
|
|
|
913
933
|
getValue: () => template?.value,
|
|
914
934
|
getCondition: () => template?.condition,
|
|
915
935
|
getContext: () => model?.context,
|
|
936
|
+
getContractModelUid: this.options.getAssignRulesModelUid,
|
|
916
937
|
};
|
|
917
938
|
|
|
918
939
|
this.rules.set(id, { rule, state: { deps: new Set(), depDisposers: [], runSeq: 0, scheduledAtWriteSeq: 0 } });
|
|
@@ -1662,7 +1683,7 @@ export class RuleEngine {
|
|
|
1662
1683
|
}
|
|
1663
1684
|
}
|
|
1664
1685
|
|
|
1665
|
-
const evalCtx = this.createRuleEvaluationContext(baseCtx, collector, targetNamePath);
|
|
1686
|
+
const evalCtx = this.createRuleEvaluationContext(baseCtx, collector, targetNamePath, rule.getContractModelUid?.());
|
|
1666
1687
|
return { collector, evalCtx, rawValue, isRunJS };
|
|
1667
1688
|
}
|
|
1668
1689
|
|
|
@@ -2001,7 +2022,12 @@ export class RuleEngine {
|
|
|
2001
2022
|
return canOverwrite;
|
|
2002
2023
|
}
|
|
2003
2024
|
|
|
2004
|
-
private createRuleEvaluationContext(
|
|
2025
|
+
private createRuleEvaluationContext(
|
|
2026
|
+
baseCtx: any,
|
|
2027
|
+
collector: DepCollector,
|
|
2028
|
+
targetNamePath: NamePath | null,
|
|
2029
|
+
contractModelUid?: string,
|
|
2030
|
+
) {
|
|
2005
2031
|
const trackingFormValues = this.options.createTrackingFormValues(collector);
|
|
2006
2032
|
const ctx: any = new FlowContext();
|
|
2007
2033
|
try {
|
|
@@ -2021,12 +2047,19 @@ export class RuleEngine {
|
|
|
2021
2047
|
}
|
|
2022
2048
|
|
|
2023
2049
|
const delegatedResolveJsonTemplate =
|
|
2024
|
-
typeof ctx.resolveJsonTemplate === 'function'
|
|
2050
|
+
typeof ctx.resolveJsonTemplate === 'function'
|
|
2051
|
+
? (ctx.resolveJsonTemplate.bind(ctx) as (
|
|
2052
|
+
template: unknown,
|
|
2053
|
+
options?: ResolveJsonTemplateOptions,
|
|
2054
|
+
) => Promise<unknown>)
|
|
2055
|
+
: undefined;
|
|
2025
2056
|
ctx.defineMethod('resolveJsonTemplate', async (template: unknown) => {
|
|
2026
2057
|
const tokenStore = this.createLocalTemplateTokenStore();
|
|
2027
2058
|
const localResolved = this.resolveLocalFormValuesTemplates(baseCtx, template, collector, tokenStore);
|
|
2028
2059
|
const nextTemplate = localResolved.matched ? localResolved.value : template;
|
|
2029
|
-
const resolved = delegatedResolveJsonTemplate
|
|
2060
|
+
const resolved = delegatedResolveJsonTemplate
|
|
2061
|
+
? await delegatedResolveJsonTemplate(nextTemplate, contractModelUid ? { contractModelUid } : undefined)
|
|
2062
|
+
: nextTemplate;
|
|
2030
2063
|
return this.restoreLocalTemplateTokens(resolved, tokenStore);
|
|
2031
2064
|
});
|
|
2032
2065
|
|
|
@@ -2038,7 +2071,7 @@ export class RuleEngine {
|
|
|
2038
2071
|
// - parentItem:上级项(同结构,可链式 parentItem.parentItem...)
|
|
2039
2072
|
// 计算顺序:
|
|
2040
2073
|
// 1) 优先按 targetNamePath 从 formValues 构建“关联链 item”
|
|
2041
|
-
// 2)
|
|
2074
|
+
// 2) 若无法构建(例如目标字段是顶层非关联字段),回退到上游显式注入的 baseCtx.item
|
|
2042
2075
|
// (如 PopupSubTable 新增弹窗传入的 parentItem 链)
|
|
2043
2076
|
let itemCached: any;
|
|
2044
2077
|
let itemCachedReady = false;
|
|
@@ -2209,9 +2242,23 @@ export class RuleEngine {
|
|
|
2209
2242
|
parentItem,
|
|
2210
2243
|
};
|
|
2211
2244
|
};
|
|
2212
|
-
const
|
|
2213
|
-
|
|
2214
|
-
|
|
2245
|
+
const blockCtx = this.options.getBlockContext();
|
|
2246
|
+
const formRootItem = (() => {
|
|
2247
|
+
try {
|
|
2248
|
+
const item = blockCtx?.item as RuntimeItemChain | undefined;
|
|
2249
|
+
if (!item || typeof item !== 'object') return undefined;
|
|
2250
|
+
return item.value === blockCtx?.formValues ? item : undefined;
|
|
2251
|
+
} catch {
|
|
2252
|
+
return undefined;
|
|
2253
|
+
}
|
|
2254
|
+
})();
|
|
2255
|
+
const defaultRoot = {
|
|
2256
|
+
...buildNode(trackingFormValues, formRootItem?.index, formRootItem?.length, formRootItem?.parentItem),
|
|
2257
|
+
__is_new__: formRootItem?.__is_new__ ?? trackingFormValues?.__is_new__,
|
|
2258
|
+
__is_stored__: formRootItem?.__is_stored__ ?? trackingFormValues?.__is_stored__,
|
|
2259
|
+
};
|
|
2260
|
+
// item 用于“关系字段目标或其子路径”场景;
|
|
2261
|
+
// 顶层非关联字段/非关联嵌套对象字段应使用 formValues。
|
|
2215
2262
|
if (!targetNamePath || !Array.isArray(targetNamePath) || !targetNamePath.length) return undefined;
|
|
2216
2263
|
if (!rootCollection?.getField) return undefined;
|
|
2217
2264
|
|
|
@@ -2219,7 +2266,9 @@ export class RuleEngine {
|
|
|
2219
2266
|
const prefix: NamePath = [];
|
|
2220
2267
|
let collection = rootCollection;
|
|
2221
2268
|
|
|
2222
|
-
|
|
2269
|
+
// The target itself can be an association. Keep that final association in the item chain so
|
|
2270
|
+
// ctx.item.parentItem continues to point at the containing row instead of skipping directly to the form root.
|
|
2271
|
+
for (let i = 0; i < targetNamePath.length; i++) {
|
|
2223
2272
|
const seg = targetNamePath[i];
|
|
2224
2273
|
if (typeof seg !== 'string') break;
|
|
2225
2274
|
|
|
@@ -2231,9 +2280,12 @@ export class RuleEngine {
|
|
|
2231
2280
|
|
|
2232
2281
|
if (toMany) {
|
|
2233
2282
|
const next = targetNamePath[i + 1];
|
|
2234
|
-
if (typeof next
|
|
2235
|
-
|
|
2236
|
-
|
|
2283
|
+
if (typeof next === 'number') {
|
|
2284
|
+
prefix.push(next);
|
|
2285
|
+
i += 1;
|
|
2286
|
+
} else if (i !== targetNamePath.length - 1) {
|
|
2287
|
+
break;
|
|
2288
|
+
}
|
|
2237
2289
|
}
|
|
2238
2290
|
|
|
2239
2291
|
const targetCollection = field?.targetCollection;
|
|
@@ -2248,9 +2300,11 @@ export class RuleEngine {
|
|
|
2248
2300
|
const assocEntry = assocEntries[idx];
|
|
2249
2301
|
const value = _.get(trackingFormValues, assocEntry.path);
|
|
2250
2302
|
const lastSeg = assocEntry.path[assocEntry.path.length - 1];
|
|
2251
|
-
const
|
|
2303
|
+
const isIndexedToManyItem = assocEntry.toMany && typeof lastSeg === 'number';
|
|
2304
|
+
const index = isIndexedToManyItem ? lastSeg : undefined;
|
|
2252
2305
|
const length = (() => {
|
|
2253
2306
|
if (!assocEntry.toMany) return undefined;
|
|
2307
|
+
if (!isIndexedToManyItem) return Array.isArray(value) ? value.length : undefined;
|
|
2254
2308
|
// assocEntry.path: [..., associationKey, rowIndex]
|
|
2255
2309
|
const listPath = assocEntry.path.slice(0, -1);
|
|
2256
2310
|
const list = _.get(trackingFormValues, listPath);
|
|
@@ -81,6 +81,10 @@ export class FormValueRuntime {
|
|
|
81
81
|
|
|
82
82
|
this.ruleEngine = new RuleEngine({
|
|
83
83
|
getBlockModelUid: () => String(this.model?.uid),
|
|
84
|
+
getAssignRulesModelUid: () => {
|
|
85
|
+
const grid = this.model?.subModels?.grid;
|
|
86
|
+
return !Array.isArray(grid) && grid?.uid ? String(grid.uid) : undefined;
|
|
87
|
+
},
|
|
84
88
|
getActionName: () => this.model?.getAclActionName?.() ?? this.model?.context?.actionName,
|
|
85
89
|
getBlockContext: () => this.model?.context,
|
|
86
90
|
getEngine: () => this.model?.context?.engine,
|
|
@@ -142,13 +146,32 @@ export class FormValueRuntime {
|
|
|
142
146
|
return this.getForm().getFieldsValue(true);
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
private toMirrorSnapshot(value:
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
private toMirrorSnapshot<T>(value: T): T {
|
|
150
|
+
const cloneValue = (input: unknown): unknown =>
|
|
151
|
+
_.cloneDeepWith(input, (item: unknown) => {
|
|
152
|
+
if (isObservable(item)) {
|
|
153
|
+
const plainItem = toJS(item);
|
|
154
|
+
if (plainItem !== item) return cloneValue(plainItem);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Tracking form-value array proxies hide `constructor`, while Lodash's array
|
|
158
|
+
// clone relies on it being callable.
|
|
159
|
+
if (Array.isArray(item) && typeof item.constructor !== 'function') {
|
|
160
|
+
const plainArray = new Array(item.length);
|
|
161
|
+
for (let index = 0; index < item.length; index++) {
|
|
162
|
+
if (Object.hasOwn(item, index)) {
|
|
163
|
+
plainArray[index] = cloneValue(item[index]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return plainArray;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!item || typeof item !== 'object') return undefined;
|
|
170
|
+
if (Array.isArray(item) || _.isPlainObject(item)) return undefined;
|
|
171
|
+
return item;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return cloneValue(value) as T;
|
|
152
175
|
}
|
|
153
176
|
|
|
154
177
|
canApplyDefaultValuePatch(namePath: NamePath, resolved: any) {
|
|
@@ -892,11 +915,12 @@ export class FormValueRuntime {
|
|
|
892
915
|
const changedPaths: NamePath[] = [];
|
|
893
916
|
|
|
894
917
|
if (!Array.isArray(patch)) {
|
|
895
|
-
const patchEntries = Object.entries(patch || {})
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
918
|
+
const patchEntries = Object.entries(patch || {})
|
|
919
|
+
.map(([pathKey, rawValue]) => [pathKey, this.toMirrorSnapshot(rawValue)] as const)
|
|
920
|
+
.filter(([pathKey, value]) => {
|
|
921
|
+
if (shouldSkipByLinkageScope(pathKey)) return false;
|
|
922
|
+
return !_.isEqual(this.getFormValueAtPath([pathKey]), value);
|
|
923
|
+
});
|
|
900
924
|
const patchToApply = Object.fromEntries(patchEntries);
|
|
901
925
|
const patchKeys = patchEntries.map(([pathKey]) => pathKey);
|
|
902
926
|
if (!patchKeys.length) {
|
|
@@ -907,8 +931,7 @@ export class FormValueRuntime {
|
|
|
907
931
|
}
|
|
908
932
|
this.suppressFormCallbackDepth++;
|
|
909
933
|
try {
|
|
910
|
-
for (const [pathKey,
|
|
911
|
-
const value = isObservable(rawValue) ? toJS(rawValue) : rawValue;
|
|
934
|
+
for (const [pathKey, value] of patchEntries) {
|
|
912
935
|
if (typeof form.setFieldValue === 'function') {
|
|
913
936
|
form.setFieldValue(pathKey, value);
|
|
914
937
|
} else if (typeof (form as any).setFields === 'function') {
|
|
@@ -969,7 +992,7 @@ export class FormValueRuntime {
|
|
|
969
992
|
const namePath = this.resolveNamePath(callerCtx, item.path);
|
|
970
993
|
const pathKey = namePathToPathKey(namePath);
|
|
971
994
|
const rawValue = item.value;
|
|
972
|
-
const value =
|
|
995
|
+
const value = this.toMirrorSnapshot(rawValue);
|
|
973
996
|
|
|
974
997
|
if (shouldSkipByLinkageScope(pathKey)) continue;
|
|
975
998
|
|
|
@@ -1108,17 +1131,18 @@ export class FormValueRuntime {
|
|
|
1108
1131
|
if (!form) return;
|
|
1109
1132
|
if (source === 'override' && this.findUserEditedHit(pathKey)) return;
|
|
1110
1133
|
|
|
1134
|
+
const value = this.toMirrorSnapshot(nextValue);
|
|
1111
1135
|
const prevValue = _.get(this.valuesMirror, namePath);
|
|
1112
|
-
if (_.isEqual(prevValue,
|
|
1136
|
+
if (_.isEqual(prevValue, value)) return;
|
|
1113
1137
|
|
|
1114
1138
|
this.writeSeq += 1;
|
|
1115
1139
|
const writeSeq = this.writeSeq;
|
|
1116
1140
|
|
|
1117
1141
|
this.suppressFormCallbackDepth++;
|
|
1118
1142
|
try {
|
|
1119
|
-
form.setFieldValue?.(namePath,
|
|
1120
|
-
_.set(this.valuesMirror, namePath, this.toMirrorSnapshot(
|
|
1121
|
-
this.syncMountedFieldModelValue(namePath,
|
|
1143
|
+
form.setFieldValue?.(namePath, value);
|
|
1144
|
+
_.set(this.valuesMirror, namePath, this.toMirrorSnapshot(value));
|
|
1145
|
+
this.syncMountedFieldModelValue(namePath, value);
|
|
1122
1146
|
this.bumpChangeTick();
|
|
1123
1147
|
} finally {
|
|
1124
1148
|
this.suppressFormCallbackDepth--;
|
|
@@ -28,6 +28,7 @@ import { observer } from '@formily/reactive-react';
|
|
|
28
28
|
import React, { useEffect, useMemo, useState, useRef } from 'react';
|
|
29
29
|
import { useTranslation } from 'react-i18next';
|
|
30
30
|
import { buildRecordPickerPopupContextInputArgs, RecordPickerContent } from '../RecordPickerFieldModel';
|
|
31
|
+
import { buildOpenerUids } from '../recordSelectShared';
|
|
31
32
|
import { AssociationFieldModel } from '../AssociationFieldModel';
|
|
32
33
|
import { adjustColumnOrder } from '../../../blocks/table/utils';
|
|
33
34
|
import { isSubTableColumnFieldComponentContext } from '../SubTableFieldModel/SubTableColumnModel';
|
|
@@ -653,6 +654,11 @@ PopupSubTableFieldModel.registerFlow({
|
|
|
653
654
|
const parentItemOptions = ctx?.getPropertyOptions?.('item');
|
|
654
655
|
const itemIndex = Array.isArray(ctx.model?.props?.value) ? ctx.model.props.value.length : 0;
|
|
655
656
|
const itemLength = itemIndex + 1;
|
|
657
|
+
const associationName = ctx.collectionField?.resourceName;
|
|
658
|
+
const sourceId = parentItem?.value
|
|
659
|
+
? ctx.collectionField?.collection?.getFilterByTK?.(parentItem.value)
|
|
660
|
+
: undefined;
|
|
661
|
+
const openerUids = buildOpenerUids(ctx, ctx.inputArgs);
|
|
656
662
|
ctx.viewer.open({
|
|
657
663
|
type: openMode,
|
|
658
664
|
width: sizeToWidthMap[openMode][size],
|
|
@@ -663,12 +669,14 @@ PopupSubTableFieldModel.registerFlow({
|
|
|
663
669
|
scene: 'subForm',
|
|
664
670
|
dataSourceKey: ctx.collection.dataSourceKey,
|
|
665
671
|
collectionName: ctx.collectionField?.target,
|
|
672
|
+
...(associationName && sourceId != null ? { associationName, sourceId } : {}),
|
|
666
673
|
collectionField: ctx.collectionField,
|
|
667
674
|
parentItem,
|
|
668
675
|
parentItemMeta: parentItemOptions?.meta,
|
|
669
676
|
parentItemResolver: parentItemOptions?.resolveOnServer,
|
|
670
677
|
itemIndex,
|
|
671
678
|
itemLength,
|
|
679
|
+
openerUids,
|
|
672
680
|
},
|
|
673
681
|
content: () => <EditFormContent model={ctx.model} scene="create" />,
|
|
674
682
|
styles: {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { PopupSubTableFieldModel } from '../PopupSubTableFieldModel';
|
|
12
|
+
import { PopupSubTableEditActionModel } from '../actions/PopupSubTableEditActionModel';
|
|
13
|
+
|
|
14
|
+
function getOpenViewHandler(modelClass: typeof PopupSubTableFieldModel | typeof PopupSubTableEditActionModel) {
|
|
15
|
+
const flow = modelClass.globalFlowRegistry.getFlow('popupSettings');
|
|
16
|
+
const step = flow?.getStep('openView');
|
|
17
|
+
const handler = step?.serialize().handler;
|
|
18
|
+
|
|
19
|
+
if (!handler) {
|
|
20
|
+
throw new Error('popupSettings.openView handler is not registered');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return handler;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createContext(record?: Record<string, unknown>, parentRecord: Record<string, unknown> = { id: 1 }) {
|
|
27
|
+
const open = vi.fn();
|
|
28
|
+
const sourceCollection = {
|
|
29
|
+
getFilterByTK: vi.fn((sourceRecord: Record<string, unknown>) => sourceRecord.id),
|
|
30
|
+
};
|
|
31
|
+
const model = {
|
|
32
|
+
uid: 'popup-subtable-uid',
|
|
33
|
+
props: {
|
|
34
|
+
value: record ? [record] : [],
|
|
35
|
+
},
|
|
36
|
+
context: {
|
|
37
|
+
inputArgs: {},
|
|
38
|
+
},
|
|
39
|
+
flowEngine: {
|
|
40
|
+
context: {
|
|
41
|
+
themeToken: {
|
|
42
|
+
colorBgLayout: '#fff',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
open,
|
|
50
|
+
context: {
|
|
51
|
+
inputArgs: {},
|
|
52
|
+
item: {
|
|
53
|
+
value: parentRecord,
|
|
54
|
+
},
|
|
55
|
+
view: {
|
|
56
|
+
inputArgs: {
|
|
57
|
+
viewUid: 'parent-popup-uid',
|
|
58
|
+
openerUids: ['root-page-uid'],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
viewer: { open },
|
|
62
|
+
layoutContentElement: {},
|
|
63
|
+
model,
|
|
64
|
+
associationModel: model,
|
|
65
|
+
collection: {
|
|
66
|
+
dataSourceKey: 'main',
|
|
67
|
+
filterTargetKey: 'id',
|
|
68
|
+
},
|
|
69
|
+
collectionField: {
|
|
70
|
+
target: 'roles',
|
|
71
|
+
resourceName: 'users.roles',
|
|
72
|
+
collection: sourceCollection,
|
|
73
|
+
},
|
|
74
|
+
record,
|
|
75
|
+
getFormValues: () => ({ id: 1 }),
|
|
76
|
+
getPropertyOptions: () => undefined,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('PopupSubTable popup context', () => {
|
|
82
|
+
it('passes popup and parent record context to the add-new popup', () => {
|
|
83
|
+
const { context, open } = createContext();
|
|
84
|
+
const handler = getOpenViewHandler(PopupSubTableFieldModel);
|
|
85
|
+
|
|
86
|
+
handler(context, { mode: 'drawer', size: 'medium' });
|
|
87
|
+
|
|
88
|
+
expect(open).toHaveBeenCalledOnce();
|
|
89
|
+
expect(open.mock.calls[0][0].inputArgs).toMatchObject({
|
|
90
|
+
openerUids: ['root-page-uid', 'parent-popup-uid'],
|
|
91
|
+
associationName: 'users.roles',
|
|
92
|
+
sourceId: 1,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('passes popup and parent record context to the edit popup', () => {
|
|
97
|
+
const { context, open } = createContext({ id: 2, name: 'Member' });
|
|
98
|
+
const handler = getOpenViewHandler(PopupSubTableEditActionModel);
|
|
99
|
+
|
|
100
|
+
handler(context, { mode: 'dialog', size: 'medium' });
|
|
101
|
+
|
|
102
|
+
expect(open).toHaveBeenCalledOnce();
|
|
103
|
+
expect(open.mock.calls[0][0].inputArgs).toMatchObject({
|
|
104
|
+
openerUids: ['root-page-uid', 'parent-popup-uid'],
|
|
105
|
+
associationName: 'users.roles',
|
|
106
|
+
sourceId: 1,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('does not expose a parent record reference before the parent record is persisted', () => {
|
|
111
|
+
const { context, open } = createContext(undefined, { nickname: 'Draft user' });
|
|
112
|
+
const handler = getOpenViewHandler(PopupSubTableFieldModel);
|
|
113
|
+
|
|
114
|
+
handler(context, { mode: 'drawer', size: 'medium' });
|
|
115
|
+
|
|
116
|
+
expect(open).toHaveBeenCalledOnce();
|
|
117
|
+
expect(open.mock.calls[0][0].inputArgs).not.toHaveProperty('associationName');
|
|
118
|
+
expect(open.mock.calls[0][0].inputArgs).not.toHaveProperty('sourceId');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -17,6 +17,7 @@ import { capitalize } from 'lodash';
|
|
|
17
17
|
import '../../../../base/ActionModel';
|
|
18
18
|
import { ActionModel, ActionWithoutPermission } from '../../../../base/ActionModelCore';
|
|
19
19
|
import { SkeletonFallback } from '../../../../../components/SkeletonFallback';
|
|
20
|
+
import { buildOpenerUids } from '../../recordSelectShared';
|
|
20
21
|
import { bindPopupSubTableBeforeClose } from './popupSubTableBeforeClose';
|
|
21
22
|
|
|
22
23
|
function FieldWithoutPermissionPlaceholder({ targetModel, children }) {
|
|
@@ -29,7 +30,7 @@ function FieldWithoutPermissionPlaceholder({ targetModel, children }) {
|
|
|
29
30
|
const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} > `;
|
|
30
31
|
const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} > ` : '';
|
|
31
32
|
return `${dataSourcePrefix}${collectionPrefix}${name}`;
|
|
32
|
-
}, []);
|
|
33
|
+
}, [collection, dataSource.displayName, dataSource.key, name, t]);
|
|
33
34
|
const { actionName } = fieldModel.forbidden || {};
|
|
34
35
|
const messageValue = useMemo(() => {
|
|
35
36
|
return t(
|
|
@@ -39,7 +40,7 @@ function FieldWithoutPermissionPlaceholder({ targetModel, children }) {
|
|
|
39
40
|
actionName: t(capitalize(actionName)),
|
|
40
41
|
},
|
|
41
42
|
).replaceAll('>', '>');
|
|
42
|
-
}, [nameValue, t]);
|
|
43
|
+
}, [actionName, nameValue, t]);
|
|
43
44
|
return <Tooltip title={messageValue}>{children}</Tooltip>;
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -347,6 +348,11 @@ PopupSubTableEditActionModel.registerFlow({
|
|
|
347
348
|
return undefined;
|
|
348
349
|
}
|
|
349
350
|
})();
|
|
351
|
+
const associationName = ctx.collectionField?.resourceName;
|
|
352
|
+
const sourceId = parentItem?.value
|
|
353
|
+
? ctx.collectionField?.collection?.getFilterByTK?.(parentItem.value)
|
|
354
|
+
: undefined;
|
|
355
|
+
const openerUids = buildOpenerUids(ctx, ctx.inputArgs);
|
|
350
356
|
ctx.viewer.open({
|
|
351
357
|
type: openMode,
|
|
352
358
|
width: sizeToWidthMap[openMode][size],
|
|
@@ -357,6 +363,7 @@ PopupSubTableEditActionModel.registerFlow({
|
|
|
357
363
|
scene: 'subForm',
|
|
358
364
|
dataSourceKey: ctx.collection.dataSourceKey,
|
|
359
365
|
collectionName: ctx.collectionField?.target,
|
|
366
|
+
...(associationName && sourceId != null ? { associationName, sourceId } : {}),
|
|
360
367
|
collectionField: ctx.collectionField,
|
|
361
368
|
record: ctx.record,
|
|
362
369
|
parentItem,
|
|
@@ -364,6 +371,7 @@ PopupSubTableEditActionModel.registerFlow({
|
|
|
364
371
|
parentItemResolver: parentItemOptions?.resolveOnServer,
|
|
365
372
|
itemIndex,
|
|
366
373
|
itemLength,
|
|
374
|
+
openerUids,
|
|
367
375
|
},
|
|
368
376
|
content: () => <EditFormContent model={ctx.model} />,
|
|
369
377
|
styles: {
|
|
@@ -243,6 +243,52 @@ describe('RecordPickerFieldModel item context', () => {
|
|
|
243
243
|
expect(associationOpts.serverOnlyWhenContextParams).toBe(true);
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
+
it('builds whole-association descriptors for current and recursive parent item values', async () => {
|
|
247
|
+
const people = {
|
|
248
|
+
name: 'people',
|
|
249
|
+
dataSourceKey: 'main',
|
|
250
|
+
filterTargetKey: 'id',
|
|
251
|
+
getFields: () => [],
|
|
252
|
+
getField: () => undefined,
|
|
253
|
+
} as any;
|
|
254
|
+
const owner = {
|
|
255
|
+
name: 'owner',
|
|
256
|
+
target: 'people',
|
|
257
|
+
targetCollection: people,
|
|
258
|
+
isAssociationField: () => true,
|
|
259
|
+
} as any;
|
|
260
|
+
const collection = {
|
|
261
|
+
name: 'tasks',
|
|
262
|
+
dataSourceKey: 'main',
|
|
263
|
+
getFields: () => [owner],
|
|
264
|
+
getField: (name: string) => (name === 'owner' ? owner : undefined),
|
|
265
|
+
} as any;
|
|
266
|
+
const options = createAssociationItemChainContextPropertyOptions({
|
|
267
|
+
t: (value) => value,
|
|
268
|
+
title: 'Current item',
|
|
269
|
+
collectionAccessor: () => collection,
|
|
270
|
+
propertiesAccessor: (ctx) => ctx.item.value,
|
|
271
|
+
resolverPropertiesAccessor: () => ({ owner: { id: 7 } }),
|
|
272
|
+
parentCollectionAccessor: () => collection,
|
|
273
|
+
parentAccessors: {
|
|
274
|
+
parentPropertiesAccessor: () => ({ owner: { id: 8 } }),
|
|
275
|
+
parentItemMetaAccessor: () => undefined,
|
|
276
|
+
parentItemResolverAccessor: () => undefined,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
const meta = await options.meta();
|
|
280
|
+
const params = await meta.buildVariablesParams({
|
|
281
|
+
item: { value: { owner: { id: 7 } }, parentItem: { value: { owner: { id: 8 } } } },
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(params).toEqual({
|
|
285
|
+
parentItem: { value: { owner: { collection: 'people', dataSourceKey: 'main', filterByTk: 8 } } },
|
|
286
|
+
value: { owner: { collection: 'people', dataSourceKey: 'main', filterByTk: 7 } },
|
|
287
|
+
});
|
|
288
|
+
expect(options.resolveOnServer('value.owner.name')).toBe(true);
|
|
289
|
+
expect(options.resolveOnServer('parentItem.value.owner.name')).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
|
|
246
292
|
it('disables current item attributes for select-record popup item context', async () => {
|
|
247
293
|
const parentCtx = new FlowContext();
|
|
248
294
|
const departmentsCollection = {
|
|
@@ -158,8 +158,15 @@ vi.mock('antd-mobile', () => {
|
|
|
158
158
|
mockState.popupProps = props;
|
|
159
159
|
return props.visible ? <div data-testid="popup">{props.children}</div> : null;
|
|
160
160
|
},
|
|
161
|
-
SearchBar: ({ value, onChange }: any) => (
|
|
162
|
-
<
|
|
161
|
+
SearchBar: ({ value, onChange, onCancel, cancelText, showCancelButton }: any) => (
|
|
162
|
+
<div>
|
|
163
|
+
<input data-testid="search" value={value ?? ''} onChange={(e) => onChange?.(e.target.value)} />
|
|
164
|
+
{showCancelButton && value ? (
|
|
165
|
+
<button type="button" onClick={onCancel}>
|
|
166
|
+
{cancelText ?? '取消'}
|
|
167
|
+
</button>
|
|
168
|
+
) : null}
|
|
169
|
+
</div>
|
|
163
170
|
),
|
|
164
171
|
CheckList: MockCheckList,
|
|
165
172
|
};
|
|
@@ -289,6 +296,17 @@ describe('MobileLazySelect', () => {
|
|
|
289
296
|
resetMockState();
|
|
290
297
|
});
|
|
291
298
|
|
|
299
|
+
it('renders a translated cancel action after searching', () => {
|
|
300
|
+
renderMobileLazySelect();
|
|
301
|
+
|
|
302
|
+
openLazyPopup();
|
|
303
|
+
act(() => {
|
|
304
|
+
fireEvent.change(screen.getByTestId('search'), { target: { value: '11' } });
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
|
308
|
+
});
|
|
309
|
+
|
|
292
310
|
it('keeps pending relation records selected until confirm', () => {
|
|
293
311
|
const { onChange, rerender } = renderMobileLazySelect();
|
|
294
312
|
|