@nocobase/client-v2 3.0.0-alpha.6 → 3.0.0-alpha.8
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 +4 -30
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -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/actions/UpdateRecordActionUtils.d.ts +8 -2
- 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/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/index.mjs +106 -106
- package/lib/index.js +97 -97
- package/package.json +7 -7
- package/src/__tests__/app.test.tsx +122 -3
- package/src/__tests__/browserChecker.test.ts +37 -0
- package/src/__tests__/settings-shell.test.tsx +27 -1
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- 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 +38 -621
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -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/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- 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 +67 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- 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
- package/src/settings-app/SettingsShell.tsx +19 -4
|
@@ -86,6 +86,10 @@ export class FormValueRuntime {
|
|
|
86
86
|
|
|
87
87
|
this.ruleEngine = new RuleEngine({
|
|
88
88
|
getBlockModelUid: () => String(this.model?.uid),
|
|
89
|
+
getAssignRulesModelUid: () => {
|
|
90
|
+
const grid = this.model?.subModels?.grid;
|
|
91
|
+
return !Array.isArray(grid) && grid?.uid ? String(grid.uid) : undefined;
|
|
92
|
+
},
|
|
89
93
|
getActionName: () => this.model?.getAclActionName?.() ?? this.model?.context?.actionName,
|
|
90
94
|
getBlockContext: () => this.model?.context,
|
|
91
95
|
getEngine: () => this.model?.context?.engine,
|
|
@@ -190,13 +194,32 @@ export class FormValueRuntime {
|
|
|
190
194
|
return values;
|
|
191
195
|
}
|
|
192
196
|
|
|
193
|
-
private toMirrorSnapshot(value:
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
private toMirrorSnapshot<T>(value: T): T {
|
|
198
|
+
const cloneValue = (input: unknown): unknown =>
|
|
199
|
+
_.cloneDeepWith(input, (item: unknown) => {
|
|
200
|
+
if (isObservable(item)) {
|
|
201
|
+
const plainItem = toJS(item);
|
|
202
|
+
if (plainItem !== item) return cloneValue(plainItem);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Tracking form-value array proxies hide `constructor`, while Lodash's array
|
|
206
|
+
// clone relies on it being callable.
|
|
207
|
+
if (Array.isArray(item) && typeof item.constructor !== 'function') {
|
|
208
|
+
const plainArray = new Array(item.length);
|
|
209
|
+
for (let index = 0; index < item.length; index++) {
|
|
210
|
+
if (Object.hasOwn(item, index)) {
|
|
211
|
+
plainArray[index] = cloneValue(item[index]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return plainArray;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!item || typeof item !== 'object') return undefined;
|
|
218
|
+
if (Array.isArray(item) || _.isPlainObject(item)) return undefined;
|
|
219
|
+
return item;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return cloneValue(value) as T;
|
|
200
223
|
}
|
|
201
224
|
|
|
202
225
|
canApplyDefaultValuePatch(namePath: NamePath, resolved: any) {
|
|
@@ -940,11 +963,12 @@ export class FormValueRuntime {
|
|
|
940
963
|
const changedPaths: NamePath[] = [];
|
|
941
964
|
|
|
942
965
|
if (!Array.isArray(patch)) {
|
|
943
|
-
const patchEntries = Object.entries(patch || {})
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
966
|
+
const patchEntries = Object.entries(patch || {})
|
|
967
|
+
.map(([pathKey, rawValue]) => [pathKey, this.toMirrorSnapshot(rawValue)] as const)
|
|
968
|
+
.filter(([pathKey, value]) => {
|
|
969
|
+
if (shouldSkipByLinkageScope(pathKey)) return false;
|
|
970
|
+
return !_.isEqual(this.getFormValueAtPath([pathKey]), value);
|
|
971
|
+
});
|
|
948
972
|
const patchToApply = Object.fromEntries(patchEntries);
|
|
949
973
|
const patchKeys = patchEntries.map(([pathKey]) => pathKey);
|
|
950
974
|
if (!patchKeys.length) {
|
|
@@ -955,8 +979,7 @@ export class FormValueRuntime {
|
|
|
955
979
|
}
|
|
956
980
|
this.suppressFormCallbackDepth++;
|
|
957
981
|
try {
|
|
958
|
-
for (const [pathKey,
|
|
959
|
-
const value = isObservable(rawValue) ? toJS(rawValue) : rawValue;
|
|
982
|
+
for (const [pathKey, value] of patchEntries) {
|
|
960
983
|
if (typeof form.setFieldValue === 'function') {
|
|
961
984
|
form.setFieldValue(pathKey, value);
|
|
962
985
|
} else if (typeof (form as any).setFields === 'function') {
|
|
@@ -1017,7 +1040,7 @@ export class FormValueRuntime {
|
|
|
1017
1040
|
const namePath = this.resolveNamePath(callerCtx, item.path);
|
|
1018
1041
|
const pathKey = namePathToPathKey(namePath);
|
|
1019
1042
|
const rawValue = item.value;
|
|
1020
|
-
const value =
|
|
1043
|
+
const value = this.toMirrorSnapshot(rawValue);
|
|
1021
1044
|
|
|
1022
1045
|
if (shouldSkipByLinkageScope(pathKey)) continue;
|
|
1023
1046
|
|
|
@@ -1156,17 +1179,18 @@ export class FormValueRuntime {
|
|
|
1156
1179
|
if (!form) return;
|
|
1157
1180
|
if (source === 'override' && this.findUserEditedHit(pathKey)) return;
|
|
1158
1181
|
|
|
1182
|
+
const value = this.toMirrorSnapshot(nextValue);
|
|
1159
1183
|
const prevValue = _.get(this.valuesMirror, namePath);
|
|
1160
|
-
if (_.isEqual(prevValue,
|
|
1184
|
+
if (_.isEqual(prevValue, value)) return;
|
|
1161
1185
|
|
|
1162
1186
|
this.writeSeq += 1;
|
|
1163
1187
|
const writeSeq = this.writeSeq;
|
|
1164
1188
|
|
|
1165
1189
|
this.suppressFormCallbackDepth++;
|
|
1166
1190
|
try {
|
|
1167
|
-
form.setFieldValue?.(namePath,
|
|
1168
|
-
_.set(this.valuesMirror, namePath, this.toMirrorSnapshot(
|
|
1169
|
-
this.syncMountedFieldModelValue(namePath,
|
|
1191
|
+
form.setFieldValue?.(namePath, value);
|
|
1192
|
+
_.set(this.valuesMirror, namePath, this.toMirrorSnapshot(value));
|
|
1193
|
+
this.syncMountedFieldModelValue(namePath, value);
|
|
1170
1194
|
this.bumpChangeTick();
|
|
1171
1195
|
} finally {
|
|
1172
1196
|
this.suppressFormCallbackDepth--;
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
import { LockOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
|
11
11
|
import { css } from '@emotion/css';
|
|
12
|
-
import type { PropertyMetaFactory } from '@nocobase/flow-engine';
|
|
12
|
+
import type { FlowModelOptions, PropertyMetaFactory } from '@nocobase/flow-engine';
|
|
13
13
|
import {
|
|
14
14
|
Droppable,
|
|
15
15
|
tExpr,
|
|
16
16
|
FlowsFloatContextMenu,
|
|
17
17
|
DragHandler,
|
|
18
18
|
MemoFlowModelRenderer,
|
|
19
|
+
createCurrentRecordMetaFactory,
|
|
19
20
|
createRecordMetaFactory,
|
|
20
21
|
createRecordResolveOnServerWithLocal,
|
|
21
22
|
ElementProxy,
|
|
@@ -51,6 +52,14 @@ function getRecordRenderSignature(record: any) {
|
|
|
51
52
|
export class JSColumnModel extends TableCustomColumnModel {
|
|
52
53
|
// Stable per‑instance render component to avoid remounts across re-renders
|
|
53
54
|
private _RenderComponent?: React.ComponentType;
|
|
55
|
+
|
|
56
|
+
onInit(options: FlowModelOptions): void {
|
|
57
|
+
super.onInit(options);
|
|
58
|
+
this.context.defineProperty('record', {
|
|
59
|
+
meta: createCurrentRecordMetaFactory(this.context, () => this.context.collection),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
54
63
|
renderHiddenInConfig() {
|
|
55
64
|
return (
|
|
56
65
|
<Tooltip
|
|
@@ -11,17 +11,62 @@ import { FlowEngine } from '@nocobase/flow-engine';
|
|
|
11
11
|
import { describe, expect, it, vi } from 'vitest';
|
|
12
12
|
import { JSColumnModel } from '../JSColumnModel';
|
|
13
13
|
|
|
14
|
-
describe('JSColumnModel
|
|
14
|
+
describe('JSColumnModel', () => {
|
|
15
|
+
it('declares current record metadata before rendering any row', async () => {
|
|
16
|
+
const engine = new FlowEngine();
|
|
17
|
+
engine.registerModels({ JSColumnModel });
|
|
18
|
+
const model = engine.createModel<JSColumnModel>({
|
|
19
|
+
use: 'JSColumnModel',
|
|
20
|
+
uid: 'js-column-record-meta',
|
|
21
|
+
props: {
|
|
22
|
+
width: 200,
|
|
23
|
+
title: 'JS column',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
engine.context.dataSourceManager.getDataSource('main').addCollection({
|
|
28
|
+
name: 'users',
|
|
29
|
+
filterTargetKey: 'id',
|
|
30
|
+
fields: [
|
|
31
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
32
|
+
{ name: 'age', type: 'integer', interface: 'integer' },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const collection = engine.context.dataSourceManager.getCollection('main', 'users');
|
|
37
|
+
model.context.defineProperty('collection', {
|
|
38
|
+
value: collection,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const recordNode = model.context.getPropertyMetaTree().find((node) => node.name === 'record');
|
|
42
|
+
const recordFields =
|
|
43
|
+
typeof recordNode?.children === 'function' ? await recordNode.children() : recordNode?.children || [];
|
|
44
|
+
|
|
45
|
+
expect(model.context.record).toBeUndefined();
|
|
46
|
+
expect(recordNode).toMatchObject({
|
|
47
|
+
name: 'record',
|
|
48
|
+
title: 'Current record',
|
|
49
|
+
paths: ['record'],
|
|
50
|
+
});
|
|
51
|
+
expect(recordFields).toEqual(
|
|
52
|
+
expect.arrayContaining([
|
|
53
|
+
expect.objectContaining({ name: 'id', paths: ['record', 'id'] }),
|
|
54
|
+
expect.objectContaining({ name: 'age', paths: ['record', 'age'] }),
|
|
55
|
+
]),
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
15
59
|
it('changes renderer key and invalidates beforeRender cache when row content changes', () => {
|
|
16
60
|
const engine = new FlowEngine();
|
|
17
|
-
|
|
61
|
+
engine.registerModels({ JSColumnModel });
|
|
62
|
+
const model = engine.createModel<JSColumnModel>({
|
|
63
|
+
use: 'JSColumnModel',
|
|
18
64
|
uid: 'js-column-row-refresh',
|
|
19
|
-
flowEngine: engine,
|
|
20
65
|
props: {
|
|
21
66
|
width: 200,
|
|
22
67
|
title: 'JS column',
|
|
23
68
|
},
|
|
24
|
-
}
|
|
69
|
+
});
|
|
25
70
|
|
|
26
71
|
engine.context.dataSourceManager.getDataSource('main').addCollection({
|
|
27
72
|
name: 'users',
|
|
@@ -39,12 +84,14 @@ describe('JSColumnModel row refresh', () => {
|
|
|
39
84
|
});
|
|
40
85
|
|
|
41
86
|
const column = model.getColumnProps();
|
|
42
|
-
const
|
|
87
|
+
const firstRecord = { id: 1, age: 3, workyears: 39.2 };
|
|
88
|
+
const first = column.render(null, firstRecord, 0) as any;
|
|
43
89
|
const fork = model.getFork('1') as any;
|
|
44
90
|
const invalidateFlowCache = vi.fn();
|
|
45
91
|
fork.invalidateFlowCache = invalidateFlowCache;
|
|
46
92
|
const second = column.render(null, { id: 1, age: 37, workyears: 39.2 }, 0) as any;
|
|
47
93
|
|
|
94
|
+
expect(fork.context.record).toEqual({ id: 1, age: 37, workyears: 39.2 });
|
|
48
95
|
expect(first.key).not.toBe(second.key);
|
|
49
96
|
expect(invalidateFlowCache).toHaveBeenCalledWith('beforeRender');
|
|
50
97
|
});
|
|
@@ -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
|
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from '../flow/models/topbar/UserCenterTopbarActionModel';
|
|
19
19
|
import { useApp } from '../hooks/useApp';
|
|
20
20
|
import { useCurrentUserAuthStatus } from '../nocobase-buildin-plugin/currentUserAuthStatus';
|
|
21
|
+
import { addCustomAlgorithmToTheme } from '../theme/customAlgorithm';
|
|
21
22
|
import { SettingsBrand } from './SettingsBrand';
|
|
22
23
|
import { SettingsGroupNav } from './SettingsGroupNav';
|
|
23
24
|
import { SettingsSearch } from './SettingsSearch';
|
|
@@ -79,17 +80,31 @@ export const SettingsShell: FC = ({ children }) => {
|
|
|
79
80
|
const { token } = antdTheme.useToken();
|
|
80
81
|
// 设置中心的外观由主题编辑器里那条「简约」主题记录约束;读不到才用代码里的中性配色兜底。
|
|
81
82
|
const storedThemeConfig = useSettingsThemeConfig();
|
|
83
|
+
const normalizedStoredThemeConfig = useMemo(
|
|
84
|
+
() =>
|
|
85
|
+
storedThemeConfig
|
|
86
|
+
? addCustomAlgorithmToTheme({
|
|
87
|
+
...storedThemeConfig,
|
|
88
|
+
algorithm: Array.isArray(storedThemeConfig.algorithm)
|
|
89
|
+
? [...storedThemeConfig.algorithm]
|
|
90
|
+
: storedThemeConfig.algorithm,
|
|
91
|
+
})
|
|
92
|
+
: storedThemeConfig,
|
|
93
|
+
[storedThemeConfig],
|
|
94
|
+
);
|
|
82
95
|
// 顶栏和补丁样式要按**设置中心自己这套主题**算色,不能直接用上面那个 `token`——
|
|
83
96
|
// 这个组件在自己的 ConfigProvider 外面,`token` 是业务端主题的。用它的话,业务端一切暗黑,
|
|
84
97
|
// 顶栏就跟着变黑,而内容区还是简约的白,一半黑一半白。
|
|
85
98
|
const settingsToken = useMemo(
|
|
86
|
-
() => (
|
|
87
|
-
[
|
|
99
|
+
() => (normalizedStoredThemeConfig ? antdTheme.getDesignToken(normalizedStoredThemeConfig) : token),
|
|
100
|
+
[normalizedStoredThemeConfig, token],
|
|
88
101
|
);
|
|
89
102
|
const settingsShellTheme = useMemo<ThemeConfig>(
|
|
90
103
|
() =>
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
normalizedStoredThemeConfig
|
|
105
|
+
? withSettingsHeaderTheme(normalizedStoredThemeConfig, settingsToken)
|
|
106
|
+
: buildSettingsNeutralTheme(token),
|
|
107
|
+
[normalizedStoredThemeConfig, settingsToken, token],
|
|
93
108
|
);
|
|
94
109
|
const headerColors = useMemo(() => getSettingsHeaderColors(settingsToken), [settingsToken]);
|
|
95
110
|
const settingsGlobalCss = useMemo(() => buildSettingsGlobalCss(settingsToken), [settingsToken]);
|