@nocobase/client-v2 2.2.0-beta.5 → 2.2.0-beta.6
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/RouteRepository.d.ts +21 -6
- package/es/components/form/JsonTextArea.d.ts +2 -1
- package/es/components/form/VariableJsonTextArea.d.ts +19 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
- package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +4 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/models/base/GridModel.d.ts +1 -1
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
- package/es/flow-compat/FieldValidation.d.ts +2 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +143 -117
- package/lib/index.js +148 -122
- package/package.json +7 -7
- package/src/RouteRepository.ts +126 -24
- package/src/__tests__/RouteRepository.test.ts +216 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/settings-center.test.tsx +33 -0
- package/src/components/form/JsonTextArea.tsx +21 -11
- package/src/components/form/VariableJsonTextArea.tsx +175 -0
- package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
- package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/index.ts +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
- package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
- package/src/flow/actions/customVariable.tsx +1 -2
- package/src/flow/actions/linkageRules.tsx +122 -34
- package/src/flow/actions/runjs.tsx +2 -14
- package/src/flow/actions/validation.tsx +62 -30
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +33 -10
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +21 -8
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
- package/src/flow/components/DefaultValue.tsx +1 -2
- package/src/flow/components/FieldAssignRulesEditor.tsx +62 -12
- package/src/flow/components/FlowRoute.tsx +56 -11
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
- package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
- package/src/flow/models/actions/JSActionModel.tsx +2 -7
- package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
- package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
- package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
- package/src/flow/models/base/ActionModel.tsx +12 -1
- package/src/flow/models/base/GridModel.tsx +38 -7
- package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
- package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
- package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
- package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
- package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
- package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
- package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
- package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
- package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
- package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
- package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
- package/src/flow/models/fields/InputFieldModel.tsx +14 -22
- package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
- package/src/flow/models/fields/JSFieldModel.tsx +2 -7
- package/src/flow/models/fields/JSItemModel.tsx +2 -14
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
- package/src/flow-compat/FieldValidation.tsx +122 -60
- package/src/flow-compat/Popover.tsx +43 -4
- package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
- package/src/index.ts +8 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
- package/src/settings-center/AdminSettingsLayout.tsx +3 -0
- package/src/settings-center/plugin-manager/index.tsx +3 -0
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { isObservable, reaction, toJS } from '@formily/reactive';
|
|
11
|
-
import { FlowContext, FlowModel, isRunJSValue, normalizeRunJSValue
|
|
11
|
+
import { FlowContext, FlowModel, isRunJSValue, normalizeRunJSValue } from '@nocobase/flow-engine';
|
|
12
|
+
import { getValuesByPath } from '@nocobase/shared';
|
|
12
13
|
import _ from 'lodash';
|
|
13
14
|
import { dayjs } from '@nocobase/utils/client';
|
|
14
15
|
import { evaluateCondition } from './conditions';
|
|
@@ -56,7 +57,31 @@ type ObservableBinding = {
|
|
|
56
57
|
dispose: () => void;
|
|
57
58
|
};
|
|
58
59
|
|
|
59
|
-
type AssignMode = 'default' | 'assign';
|
|
60
|
+
type AssignMode = 'default' | 'assign' | 'override';
|
|
61
|
+
|
|
62
|
+
function normalizeAssignMode(mode: unknown): AssignMode {
|
|
63
|
+
if (mode === 'default') return 'default';
|
|
64
|
+
if (mode === 'override') return 'override';
|
|
65
|
+
return 'assign';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getSourceByAssignMode(mode: AssignMode): ValueSource {
|
|
69
|
+
if (mode === 'default') return 'default';
|
|
70
|
+
if (mode === 'override') return 'override';
|
|
71
|
+
return 'system';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type LocalTemplateTokenStore = {
|
|
75
|
+
values: Map<string, unknown>;
|
|
76
|
+
protect: (value: unknown) => string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ToManyAggregateSourceInfo = {
|
|
80
|
+
subPath: string;
|
|
81
|
+
arrayPath: NamePath;
|
|
82
|
+
arrayValue: unknown;
|
|
83
|
+
lastWrite?: FormValueWriteMeta;
|
|
84
|
+
};
|
|
60
85
|
|
|
61
86
|
export type RuleEngineOptions = {
|
|
62
87
|
getBlockModelUid: () => string;
|
|
@@ -74,6 +99,7 @@ export type RuleEngineOptions = {
|
|
|
74
99
|
getFormValueAtPath: (namePath: NamePath) => any;
|
|
75
100
|
setFormValues: (callerCtx: any, patch: Patch, options?: SetOptions) => Promise<void>;
|
|
76
101
|
findExplicitHit: (pathKey: string) => string | null;
|
|
102
|
+
findUserEditedHit: (pathKey: string) => string | null;
|
|
77
103
|
lastDefaultValueByPathKey: Map<string, any>;
|
|
78
104
|
lastWriteMetaByPathKey: Map<string, FormValueWriteMeta>;
|
|
79
105
|
observableBindings: Map<string, ObservableBinding>;
|
|
@@ -166,8 +192,8 @@ export class RuleEngine {
|
|
|
166
192
|
if (!templateKey) continue;
|
|
167
193
|
if (this.hasAnyNonBlockAssignRuleInstance(templateKey)) continue;
|
|
168
194
|
|
|
169
|
-
const mode
|
|
170
|
-
const source
|
|
195
|
+
const mode = normalizeAssignMode(template?.mode);
|
|
196
|
+
const source = getSourceByAssignMode(mode);
|
|
171
197
|
const enabled = template?.enable !== false;
|
|
172
198
|
const id = this.getAssignRuleBlockId(templateKey);
|
|
173
199
|
if (this.rules.has(id)) continue;
|
|
@@ -503,8 +529,8 @@ export class RuleEngine {
|
|
|
503
529
|
if (matchedTargetPaths.has(targetPath)) continue;
|
|
504
530
|
if (!this.shouldCreateBlockLevelAssignRule(targetPath)) continue;
|
|
505
531
|
for (const template of templates) {
|
|
506
|
-
const mode
|
|
507
|
-
const source
|
|
532
|
+
const mode = normalizeAssignMode(template?.mode);
|
|
533
|
+
const source = getSourceByAssignMode(mode);
|
|
508
534
|
const enabled = template?.enable !== false;
|
|
509
535
|
const id = `${prefix}${template.__key}:block`;
|
|
510
536
|
if (this.rules.has(id)) this.removeRule(id);
|
|
@@ -867,8 +893,8 @@ export class RuleEngine {
|
|
|
867
893
|
this.removeRowGridAssignRuleInstances(template.__key);
|
|
868
894
|
}
|
|
869
895
|
|
|
870
|
-
const mode
|
|
871
|
-
const source
|
|
896
|
+
const mode = normalizeAssignMode(template?.mode);
|
|
897
|
+
const source = getSourceByAssignMode(mode);
|
|
872
898
|
const enabled = template?.enable !== false;
|
|
873
899
|
const id = this.getAssignRuleInstanceId(template.__key, model);
|
|
874
900
|
|
|
@@ -1200,13 +1226,52 @@ export class RuleEngine {
|
|
|
1200
1226
|
|
|
1201
1227
|
this.commitRuleDeps(rule, state, collector);
|
|
1202
1228
|
|
|
1203
|
-
const
|
|
1204
|
-
const
|
|
1229
|
+
const toManyAggregateSource = this.getToManyAggregateSourceInfo(baseCtx, rule.getValue());
|
|
1230
|
+
const unresolvedAggregate = this.normalizeUnresolvedToManyAggregateForScalarTarget(
|
|
1231
|
+
baseCtx,
|
|
1232
|
+
ensuredTargetNamePath,
|
|
1233
|
+
resolved,
|
|
1234
|
+
toManyAggregateSource,
|
|
1235
|
+
);
|
|
1236
|
+
if (unresolvedAggregate.skip) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
const resolvedForTarget = unresolvedAggregate.hasValue ? unresolvedAggregate.value : resolved;
|
|
1240
|
+
|
|
1241
|
+
const normalizedResolved = this.normalizeResolvedValueForTarget(baseCtx, resolvedForTarget);
|
|
1242
|
+
const normalizedResolvedForAssociationTarget = this.normalizeResolvedValueForAssociationTarget(
|
|
1205
1243
|
baseCtx,
|
|
1206
1244
|
ensuredTargetNamePath,
|
|
1207
1245
|
normalizedResolved,
|
|
1208
1246
|
);
|
|
1209
1247
|
|
|
1248
|
+
const normalizedResolvedForTarget = this.normalizeAggregateArrayForScalarTarget(
|
|
1249
|
+
baseCtx,
|
|
1250
|
+
ensuredTargetNamePath,
|
|
1251
|
+
normalizedResolvedForAssociationTarget,
|
|
1252
|
+
toManyAggregateSource,
|
|
1253
|
+
);
|
|
1254
|
+
if (normalizedResolvedForTarget === SKIP_RULE_VALUE) {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (
|
|
1259
|
+
this.shouldSkipEmptyArrayAggregateForScalarTarget(baseCtx, ensuredTargetNamePath, normalizedResolvedForTarget)
|
|
1260
|
+
) {
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
if (
|
|
1265
|
+
this.shouldSkipMultiValueAggregateForScalarTarget(
|
|
1266
|
+
baseCtx,
|
|
1267
|
+
ensuredTargetNamePath,
|
|
1268
|
+
normalizedResolvedForTarget,
|
|
1269
|
+
toManyAggregateSource,
|
|
1270
|
+
)
|
|
1271
|
+
) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1210
1275
|
if (rule.source === 'default') {
|
|
1211
1276
|
const shouldApply = this.checkDefaultRuleCanApply(
|
|
1212
1277
|
normalizedResolvedForTarget,
|
|
@@ -1218,6 +1283,10 @@ export class RuleEngine {
|
|
|
1218
1283
|
);
|
|
1219
1284
|
if (!shouldApply) return;
|
|
1220
1285
|
}
|
|
1286
|
+
if (rule.source === 'override' && this.options.findUserEditedHit(ensuredTargetKey)) {
|
|
1287
|
+
disposeBinding();
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1221
1290
|
|
|
1222
1291
|
if (rule.source === 'default') {
|
|
1223
1292
|
const lastWrite = this.lastRuleWriteByTargetKey.get(ensuredTargetKey);
|
|
@@ -1231,7 +1300,7 @@ export class RuleEngine {
|
|
|
1231
1300
|
}
|
|
1232
1301
|
}
|
|
1233
1302
|
|
|
1234
|
-
if (rule.source === 'system') {
|
|
1303
|
+
if (rule.source === 'system' || rule.source === 'override') {
|
|
1235
1304
|
const lastWrite = this.options.lastWriteMetaByPathKey.get(ensuredTargetKey);
|
|
1236
1305
|
if (lastWrite?.source === 'linkage' && lastWrite.writeSeq >= scheduledAtWriteSeq) {
|
|
1237
1306
|
disposeBinding();
|
|
@@ -1255,7 +1324,7 @@ export class RuleEngine {
|
|
|
1255
1324
|
const initPatches = this.collectUpdateAssociationInitPatches(baseCtx, ensuredTargetNamePath);
|
|
1256
1325
|
if (initPatches == null) return;
|
|
1257
1326
|
|
|
1258
|
-
if (rule.source === 'system' || rule.source === 'default') {
|
|
1327
|
+
if (rule.source === 'system' || rule.source === 'default' || rule.source === 'override') {
|
|
1259
1328
|
const modelForUi = baseCtx?.model;
|
|
1260
1329
|
const fieldModelForUi = modelForUi?.subModels?.field;
|
|
1261
1330
|
if (fieldModelForUi) {
|
|
@@ -1562,6 +1631,9 @@ export class RuleEngine {
|
|
|
1562
1631
|
if (!this.shouldApplyDefaultRuleInCurrentState(baseCtx)) return false;
|
|
1563
1632
|
if (this.options.findExplicitHit(targetKey)) return false;
|
|
1564
1633
|
}
|
|
1634
|
+
if (rule.source === 'override') {
|
|
1635
|
+
if (this.options.findUserEditedHit(targetKey)) return false;
|
|
1636
|
+
}
|
|
1565
1637
|
return true;
|
|
1566
1638
|
}
|
|
1567
1639
|
|
|
@@ -1646,7 +1718,7 @@ export class RuleEngine {
|
|
|
1646
1718
|
): Promise<any> {
|
|
1647
1719
|
try {
|
|
1648
1720
|
const { code, version } = normalizeRunJSValue(rawValue);
|
|
1649
|
-
const ret = await
|
|
1721
|
+
const ret = await evalCtx.runjs(code, undefined, { version });
|
|
1650
1722
|
if (!ret?.success) {
|
|
1651
1723
|
if (seq !== state.runSeq) return SKIP_RULE_VALUE;
|
|
1652
1724
|
this.commitRuleDeps(rule, state, collector);
|
|
@@ -1685,6 +1757,213 @@ export class RuleEngine {
|
|
|
1685
1757
|
this.updateRuleDeps(rule, state, nextDeps);
|
|
1686
1758
|
}
|
|
1687
1759
|
|
|
1760
|
+
private isScalarValueTarget(baseCtx: any, targetNamePath: NamePath): boolean {
|
|
1761
|
+
const targetField = this.resolveCollectionFieldByNamePath(baseCtx, targetNamePath);
|
|
1762
|
+
if (targetField?.isAssociationField?.() && isToManyAssociationField(targetField)) {
|
|
1763
|
+
return false;
|
|
1764
|
+
}
|
|
1765
|
+
if (this.isArrayValueCollectionField(targetField)) {
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
if (targetField) {
|
|
1769
|
+
return true;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
const current = this.options.getFormValueAtPath(targetNamePath);
|
|
1773
|
+
return !Array.isArray(current);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
private shouldSkipEmptyArrayAggregateForScalarTarget(baseCtx: any, targetNamePath: NamePath, resolved: any): boolean {
|
|
1777
|
+
if (!Array.isArray(resolved) || resolved.length > 0) return false;
|
|
1778
|
+
return this.isScalarValueTarget(baseCtx, targetNamePath);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
private normalizeUnresolvedToManyAggregateForScalarTarget(
|
|
1782
|
+
baseCtx: any,
|
|
1783
|
+
targetNamePath: NamePath,
|
|
1784
|
+
resolved: any,
|
|
1785
|
+
aggregateInfo: ToManyAggregateSourceInfo | null,
|
|
1786
|
+
): { skip: boolean; hasValue: boolean; value?: unknown } {
|
|
1787
|
+
if (typeof resolved !== 'undefined') return { skip: false, hasValue: false };
|
|
1788
|
+
if (!aggregateInfo) return { skip: false, hasValue: false };
|
|
1789
|
+
if (!this.isScalarValueTarget(baseCtx, targetNamePath)) return { skip: false, hasValue: false };
|
|
1790
|
+
|
|
1791
|
+
if (Array.isArray(aggregateInfo.arrayValue) && aggregateInfo.arrayValue.length === 0) {
|
|
1792
|
+
if (this.isUserClearedToManyAggregate(aggregateInfo)) {
|
|
1793
|
+
return {
|
|
1794
|
+
skip: false,
|
|
1795
|
+
hasValue: true,
|
|
1796
|
+
value: this.getClearedAggregateValueForScalarTarget(baseCtx, targetNamePath),
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
return { skip: true, hasValue: false };
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
if (Array.isArray(aggregateInfo.arrayValue) && aggregateInfo.arrayValue.length > 0) {
|
|
1803
|
+
return { skip: true, hasValue: false };
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
return { skip: false, hasValue: false };
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
private isArrayValueCollectionField(field: unknown): boolean {
|
|
1810
|
+
if (!field || typeof field !== 'object') return false;
|
|
1811
|
+
const collectionField = field as {
|
|
1812
|
+
interface?: unknown;
|
|
1813
|
+
multiple?: unknown;
|
|
1814
|
+
schema?: { type?: unknown };
|
|
1815
|
+
type?: unknown;
|
|
1816
|
+
uiSchema?: { type?: unknown };
|
|
1817
|
+
};
|
|
1818
|
+
if (collectionField.multiple === true) return true;
|
|
1819
|
+
if (collectionField.type === 'array') return true;
|
|
1820
|
+
|
|
1821
|
+
const uiSchemaType = collectionField.uiSchema?.type ?? collectionField.schema?.type;
|
|
1822
|
+
if (uiSchemaType === 'array') return true;
|
|
1823
|
+
|
|
1824
|
+
const fieldInterface = typeof collectionField.interface === 'string' ? collectionField.interface : '';
|
|
1825
|
+
return ['multipleSelect', 'checkboxGroup', 'json'].includes(fieldInterface);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
private stringifyAggregateItem(value: unknown): string {
|
|
1829
|
+
if (value && typeof value === 'object') {
|
|
1830
|
+
try {
|
|
1831
|
+
return JSON.stringify(value);
|
|
1832
|
+
} catch {
|
|
1833
|
+
return String(value);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
return String(value);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
private normalizeAggregateArrayForScalarTarget(
|
|
1840
|
+
baseCtx: any,
|
|
1841
|
+
targetNamePath: NamePath,
|
|
1842
|
+
resolved: any,
|
|
1843
|
+
aggregateInfo: ToManyAggregateSourceInfo | null,
|
|
1844
|
+
): any {
|
|
1845
|
+
if (!Array.isArray(resolved)) return resolved;
|
|
1846
|
+
if (!this.isScalarValueTarget(baseCtx, targetNamePath)) return resolved;
|
|
1847
|
+
if (!aggregateInfo) return resolved;
|
|
1848
|
+
|
|
1849
|
+
const values = resolved.filter((item) => item !== null && typeof item !== 'undefined');
|
|
1850
|
+
if (!values.length) {
|
|
1851
|
+
if (this.isUserClearedToManyAggregate(aggregateInfo)) {
|
|
1852
|
+
return this.getClearedAggregateValueForScalarTarget(baseCtx, targetNamePath);
|
|
1853
|
+
}
|
|
1854
|
+
return SKIP_RULE_VALUE;
|
|
1855
|
+
}
|
|
1856
|
+
if (values.length === 1) return values[0];
|
|
1857
|
+
if (!this.isTextScalarTarget(baseCtx, targetNamePath)) return SKIP_RULE_VALUE;
|
|
1858
|
+
return values.map((item) => this.stringifyAggregateItem(item)).join(', ');
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
private shouldSkipMultiValueAggregateForScalarTarget(
|
|
1862
|
+
baseCtx: any,
|
|
1863
|
+
targetNamePath: NamePath,
|
|
1864
|
+
resolved: any,
|
|
1865
|
+
aggregateInfo: ToManyAggregateSourceInfo | null,
|
|
1866
|
+
): boolean {
|
|
1867
|
+
if (!aggregateInfo) return false;
|
|
1868
|
+
if (!Array.isArray(resolved) || resolved.length <= 1) return false;
|
|
1869
|
+
if (!this.isScalarValueTarget(baseCtx, targetNamePath)) return false;
|
|
1870
|
+
return !this.isTextScalarTarget(baseCtx, targetNamePath);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
private getClearedAggregateValueForScalarTarget(baseCtx: any, targetNamePath: NamePath): unknown {
|
|
1874
|
+
if (this.isTextScalarTarget(baseCtx, targetNamePath)) return '';
|
|
1875
|
+
return undefined;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
private isUserClearedToManyAggregate(aggregateInfo: ToManyAggregateSourceInfo): boolean {
|
|
1879
|
+
return aggregateInfo.lastWrite?.source === 'user';
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
private isTextScalarTarget(baseCtx: any, targetNamePath: NamePath): boolean {
|
|
1883
|
+
const targetField = this.resolveCollectionFieldByNamePath(baseCtx, targetNamePath);
|
|
1884
|
+
if (!targetField) {
|
|
1885
|
+
const current = this.options.getFormValueAtPath(targetNamePath);
|
|
1886
|
+
return typeof current === 'undefined' || current == null || typeof current === 'string';
|
|
1887
|
+
}
|
|
1888
|
+
if (targetField?.isAssociationField?.()) return false;
|
|
1889
|
+
if (this.isArrayValueCollectionField(targetField)) return false;
|
|
1890
|
+
|
|
1891
|
+
const field = targetField as {
|
|
1892
|
+
interface?: unknown;
|
|
1893
|
+
schema?: { type?: unknown };
|
|
1894
|
+
type?: unknown;
|
|
1895
|
+
uiSchema?: { type?: unknown };
|
|
1896
|
+
};
|
|
1897
|
+
const schemaType = field.uiSchema?.type ?? field.schema?.type;
|
|
1898
|
+
if (field.type === 'string' || field.type === 'text' || schemaType === 'string') return true;
|
|
1899
|
+
|
|
1900
|
+
const fieldInterface = typeof field.interface === 'string' ? field.interface : '';
|
|
1901
|
+
return [
|
|
1902
|
+
'input',
|
|
1903
|
+
'textarea',
|
|
1904
|
+
'email',
|
|
1905
|
+
'phone',
|
|
1906
|
+
'url',
|
|
1907
|
+
'uuid',
|
|
1908
|
+
'nanoid',
|
|
1909
|
+
'markdown',
|
|
1910
|
+
'richText',
|
|
1911
|
+
'vditor',
|
|
1912
|
+
'password',
|
|
1913
|
+
'color',
|
|
1914
|
+
].includes(fieldInterface);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
private getLatestWriteMetaForPath(namePath: NamePath): FormValueWriteMeta | undefined {
|
|
1918
|
+
const pathKey = namePathToPathKey(namePath);
|
|
1919
|
+
let latest = this.options.lastWriteMetaByPathKey.get(pathKey);
|
|
1920
|
+
for (const [key, meta] of this.options.lastWriteMetaByPathKey.entries()) {
|
|
1921
|
+
if (key !== pathKey && !key.startsWith(`${pathKey}.`) && !key.startsWith(`${pathKey}[`)) continue;
|
|
1922
|
+
if (!latest || meta.writeSeq > latest.writeSeq) {
|
|
1923
|
+
latest = meta;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
return latest;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
private getToManyAggregateSourceInfo(baseCtx: unknown, rawValue: unknown): ToManyAggregateSourceInfo | null {
|
|
1930
|
+
const subPath = this.extractSingleLocalFormValuesPath(rawValue);
|
|
1931
|
+
if (!subPath) return null;
|
|
1932
|
+
|
|
1933
|
+
const segs = parsePathString(subPath).filter((seg) => typeof seg !== 'object') as NamePath;
|
|
1934
|
+
if (segs.length < 2) return null;
|
|
1935
|
+
|
|
1936
|
+
let collection = this.getRootCollection() || this.getCollectionFromContext(baseCtx);
|
|
1937
|
+
if (!collection?.getField) return null;
|
|
1938
|
+
|
|
1939
|
+
const prefix: NamePath = [];
|
|
1940
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
1941
|
+
const seg = segs[i];
|
|
1942
|
+
if (typeof seg !== 'string') return null;
|
|
1943
|
+
|
|
1944
|
+
const field = collection?.getField?.(seg);
|
|
1945
|
+
if (!field?.isAssociationField?.()) return null;
|
|
1946
|
+
|
|
1947
|
+
prefix.push(seg);
|
|
1948
|
+
if (isToManyAssociationField(field)) {
|
|
1949
|
+
const directValue = this.options.getFormValueAtPath(prefix);
|
|
1950
|
+
const snapshot = this.getLocalFormValuesSnapshot(baseCtx);
|
|
1951
|
+
const snapshotValue = snapshot && typeof snapshot === 'object' ? _.get(snapshot, prefix) : undefined;
|
|
1952
|
+
return {
|
|
1953
|
+
subPath,
|
|
1954
|
+
arrayPath: [...prefix],
|
|
1955
|
+
arrayValue: typeof directValue !== 'undefined' ? directValue : snapshotValue,
|
|
1956
|
+
lastWrite: this.getLatestWriteMetaForPath(prefix),
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
collection = field.targetCollection;
|
|
1961
|
+
if (!collection?.getField) return null;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1688
1967
|
/**
|
|
1689
1968
|
* Check if default rule value can be applied.
|
|
1690
1969
|
* Default value can overwrite when:
|
|
@@ -1741,6 +2020,16 @@ export class RuleEngine {
|
|
|
1741
2020
|
ctx.defineProperty('formValues', { get: () => trackingFormValues, cache: false });
|
|
1742
2021
|
}
|
|
1743
2022
|
|
|
2023
|
+
const delegatedResolveJsonTemplate =
|
|
2024
|
+
typeof ctx.resolveJsonTemplate === 'function' ? ctx.resolveJsonTemplate.bind(ctx) : undefined;
|
|
2025
|
+
ctx.defineMethod('resolveJsonTemplate', async (template: unknown) => {
|
|
2026
|
+
const tokenStore = this.createLocalTemplateTokenStore();
|
|
2027
|
+
const localResolved = this.resolveLocalFormValuesTemplates(baseCtx, template, collector, tokenStore);
|
|
2028
|
+
const nextTemplate = localResolved.matched ? localResolved.value : template;
|
|
2029
|
+
const resolved = delegatedResolveJsonTemplate ? await delegatedResolveJsonTemplate(nextTemplate) : nextTemplate;
|
|
2030
|
+
return this.restoreLocalTemplateTokens(resolved, tokenStore);
|
|
2031
|
+
});
|
|
2032
|
+
|
|
1744
2033
|
// “当前项”链:用于多层级关系字段条件
|
|
1745
2034
|
// 语义:ctx.item -> { index?, length?, __is_new__?, __is_stored__?, value, parentItem? },其中:
|
|
1746
2035
|
// - index:仅当当前对象位于对多关联行内时存在(0-based)
|
|
@@ -1765,6 +2054,149 @@ export class RuleEngine {
|
|
|
1765
2054
|
return ctx;
|
|
1766
2055
|
}
|
|
1767
2056
|
|
|
2057
|
+
private getLocalFormValuesSnapshot(baseCtx: unknown): unknown {
|
|
2058
|
+
try {
|
|
2059
|
+
const snapshot = (baseCtx as { getFormValues?: () => unknown } | undefined)?.getFormValues?.();
|
|
2060
|
+
if (snapshot && typeof snapshot === 'object') {
|
|
2061
|
+
return snapshot;
|
|
2062
|
+
}
|
|
2063
|
+
} catch {
|
|
2064
|
+
// ignore
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
const mirror = this.options.valuesMirror;
|
|
2068
|
+
return isObservable(mirror) ? toJS(mirror) : mirror;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
private resolveLocalFormValuesPath(baseCtx: unknown, subPath: string): unknown {
|
|
2072
|
+
const snapshot = this.getLocalFormValuesSnapshot(baseCtx);
|
|
2073
|
+
if (!snapshot || typeof snapshot !== 'object') return undefined;
|
|
2074
|
+
return getValuesByPath(snapshot as object, subPath);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
private recordLocalFormValuesDep(subPath: string, collector?: DepCollector) {
|
|
2078
|
+
const segs = parsePathString(subPath).filter((seg) => typeof seg !== 'object') as NamePath;
|
|
2079
|
+
if (!segs.length) {
|
|
2080
|
+
if (collector) collector.wildcard = true;
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
recordDep(segs, collector);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
private extractSingleLocalFormValuesPath(template: unknown): string | null {
|
|
2087
|
+
if (typeof template !== 'string') return null;
|
|
2088
|
+
const single = template.match(
|
|
2089
|
+
/^\s*\{\{\s*ctx\.formValues\.([a-zA-Z_$][a-zA-Z0-9_$-]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$-]*)*)\s*\}\}\s*$/,
|
|
2090
|
+
);
|
|
2091
|
+
return single?.[1] ?? null;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
private stringifyTemplateReplacement(value: unknown): string | undefined {
|
|
2095
|
+
if (typeof value === 'undefined') return undefined;
|
|
2096
|
+
return value && typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
private createLocalTemplateTokenStore(): LocalTemplateTokenStore {
|
|
2100
|
+
const values = new Map<string, unknown>();
|
|
2101
|
+
let index = 0;
|
|
2102
|
+
return {
|
|
2103
|
+
values,
|
|
2104
|
+
protect(value: unknown) {
|
|
2105
|
+
const token = `__NOCObase_FORM_VALUE_LITERAL_${index}__`;
|
|
2106
|
+
index += 1;
|
|
2107
|
+
values.set(token, value);
|
|
2108
|
+
return token;
|
|
2109
|
+
},
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
private restoreLocalTemplateTokens(value: unknown, tokenStore: LocalTemplateTokenStore): unknown {
|
|
2114
|
+
if (!tokenStore.values.size) return value;
|
|
2115
|
+
|
|
2116
|
+
if (typeof value === 'string') {
|
|
2117
|
+
if (tokenStore.values.has(value)) {
|
|
2118
|
+
return tokenStore.values.get(value);
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
let next = value;
|
|
2122
|
+
for (const [token, replacement] of tokenStore.values.entries()) {
|
|
2123
|
+
if (!next.includes(token)) continue;
|
|
2124
|
+
next = next.split(token).join(this.stringifyTemplateReplacement(replacement) ?? '');
|
|
2125
|
+
}
|
|
2126
|
+
return next;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
if (Array.isArray(value)) {
|
|
2130
|
+
return value.map((item) => this.restoreLocalTemplateTokens(item, tokenStore));
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
if (_.isPlainObject(value)) {
|
|
2134
|
+
const restored: Record<string, unknown> = {};
|
|
2135
|
+
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
|
2136
|
+
restored[key] = this.restoreLocalTemplateTokens(item, tokenStore);
|
|
2137
|
+
}
|
|
2138
|
+
return restored;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
return value;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
private resolveLocalFormValuesTemplates(
|
|
2145
|
+
baseCtx: unknown,
|
|
2146
|
+
template: unknown,
|
|
2147
|
+
collector?: DepCollector,
|
|
2148
|
+
tokenStore?: LocalTemplateTokenStore,
|
|
2149
|
+
): { matched: boolean; value: unknown } {
|
|
2150
|
+
if (typeof template === 'string') {
|
|
2151
|
+
const single = this.extractSingleLocalFormValuesPath(template);
|
|
2152
|
+
if (single) {
|
|
2153
|
+
this.recordLocalFormValuesDep(single, collector);
|
|
2154
|
+
const resolved = this.resolveLocalFormValuesPath(baseCtx, single);
|
|
2155
|
+
if (typeof resolved !== 'undefined') {
|
|
2156
|
+
return { matched: true, value: tokenStore ? tokenStore.protect(resolved) : resolved };
|
|
2157
|
+
}
|
|
2158
|
+
return { matched: false, value: template };
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
let matched = false;
|
|
2162
|
+
const value = template.replace(
|
|
2163
|
+
/\{\{\s*ctx\.formValues\.([a-zA-Z_$][a-zA-Z0-9_$-]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$-]*)*)\s*\}\}/g,
|
|
2164
|
+
(fullMatch, subPath) => {
|
|
2165
|
+
this.recordLocalFormValuesDep(subPath, collector);
|
|
2166
|
+
const resolved = this.resolveLocalFormValuesPath(baseCtx, subPath);
|
|
2167
|
+
const replacement = this.stringifyTemplateReplacement(resolved);
|
|
2168
|
+
if (typeof replacement === 'undefined') return fullMatch;
|
|
2169
|
+
matched = true;
|
|
2170
|
+
return tokenStore ? tokenStore.protect(replacement) : replacement;
|
|
2171
|
+
},
|
|
2172
|
+
);
|
|
2173
|
+
return { matched, value };
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
if (Array.isArray(template)) {
|
|
2177
|
+
let matched = false;
|
|
2178
|
+
const value = template.map((item) => {
|
|
2179
|
+
const resolved = this.resolveLocalFormValuesTemplates(baseCtx, item, collector, tokenStore);
|
|
2180
|
+
if (resolved.matched) matched = true;
|
|
2181
|
+
return resolved.value;
|
|
2182
|
+
});
|
|
2183
|
+
return matched ? { matched, value } : { matched: false, value: template };
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
if (_.isPlainObject(template)) {
|
|
2187
|
+
let matched = false;
|
|
2188
|
+
const value: Record<string, unknown> = {};
|
|
2189
|
+
for (const [key, item] of Object.entries(template as Record<string, unknown>)) {
|
|
2190
|
+
const resolved = this.resolveLocalFormValuesTemplates(baseCtx, item, collector, tokenStore);
|
|
2191
|
+
if (resolved.matched) matched = true;
|
|
2192
|
+
value[key] = resolved.value;
|
|
2193
|
+
}
|
|
2194
|
+
return matched ? { matched, value } : { matched: false, value: template };
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
return { matched: false, value: template };
|
|
2198
|
+
}
|
|
2199
|
+
|
|
1768
2200
|
private buildItemChainValue(baseCtx: any, trackingFormValues: any, targetNamePath: NamePath | null) {
|
|
1769
2201
|
const rootCollection = this.getRootCollection() || this.getCollectionFromContext(baseCtx);
|
|
1770
2202
|
const buildNode = (value: any, index: number | undefined, length: number | undefined, parentItem: any) => {
|