@nocobase/client-v2 3.0.0-alpha.7 → 3.0.0-alpha.9
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/blocks/form/FormBlockModel.d.ts +7 -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 +125 -134
- package/lib/index.js +119 -128
- package/package.json +7 -7
- package/src/__tests__/app.test.tsx +15 -6
- package/src/__tests__/settings-center.test.tsx +222 -21
- package/src/components/AppComponents.tsx +4 -4
- 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/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- 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/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/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -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-center/AdminSettingsLayout.tsx +38 -3
|
@@ -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--;
|
|
@@ -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: {
|
|
@@ -0,0 +1,33 @@
|
|
|
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 } from 'vitest';
|
|
11
|
+
import { formatNumber } from '../DisplayNumberFieldModel';
|
|
12
|
+
|
|
13
|
+
describe('formatNumber', () => {
|
|
14
|
+
it('formats a 30-digit decimal without scientific notation or precision loss', () => {
|
|
15
|
+
expect(
|
|
16
|
+
formatNumber({
|
|
17
|
+
value: '123456789012345678901234567890',
|
|
18
|
+
formatStyle: 'normal',
|
|
19
|
+
step: '1',
|
|
20
|
+
}),
|
|
21
|
+
).toBe('123,456,789,012,345,678,901,234,567,890');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('formats the 22-digit value from the reported v2 scenario', () => {
|
|
25
|
+
expect(
|
|
26
|
+
formatNumber({
|
|
27
|
+
value: '1234567890123458152112',
|
|
28
|
+
formatStyle: 'normal',
|
|
29
|
+
step: '1',
|
|
30
|
+
}),
|
|
31
|
+
).toBe('1,234,567,890,123,458,152,112');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { fireEvent, render, screen } from '@testing-library/react';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import { InputNumberField } from '../NumberFieldModel';
|
|
14
|
+
|
|
15
|
+
describe('InputNumberField', () => {
|
|
16
|
+
it('keeps the existing number value type for regular input', () => {
|
|
17
|
+
const onChange = vi.fn();
|
|
18
|
+
|
|
19
|
+
render(<InputNumberField stringMode onChange={onChange} />);
|
|
20
|
+
fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '123' } });
|
|
21
|
+
|
|
22
|
+
expect(onChange).toHaveBeenLastCalledWith(123);
|
|
23
|
+
expect(onChange).not.toHaveBeenCalledWith('123');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('preserves a high-precision decimal string in string mode', () => {
|
|
27
|
+
const onChange = vi.fn();
|
|
28
|
+
const value = '123456789012345678901234567890';
|
|
29
|
+
|
|
30
|
+
render(<InputNumberField stringMode onChange={onChange} />);
|
|
31
|
+
fireEvent.change(screen.getByRole('spinbutton'), { target: { value } });
|
|
32
|
+
|
|
33
|
+
expect(onChange).toHaveBeenLastCalledWith(value);
|
|
34
|
+
expect(screen.getByRole('spinbutton')).toHaveValue(value);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('does not switch to scientific notation at the 22-digit boundary', () => {
|
|
38
|
+
const onChange = vi.fn();
|
|
39
|
+
const value = '1234567890123458152112';
|
|
40
|
+
|
|
41
|
+
render(<InputNumberField stringMode onChange={onChange} />);
|
|
42
|
+
fireEvent.change(screen.getByRole('spinbutton'), { target: { value } });
|
|
43
|
+
|
|
44
|
+
expect(onChange).toHaveBeenLastCalledWith(value);
|
|
45
|
+
expect(onChange).not.toHaveBeenCalledWith('1.234567890123458152112e+21');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -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
|
|
|
@@ -13,9 +13,11 @@ import { FlowModelRenderer, useFlowEngine } from '@nocobase/flow-engine';
|
|
|
13
13
|
import { Layout, Result, Tabs, theme } from 'antd';
|
|
14
14
|
import React, { useEffect, useMemo, useRef } from 'react';
|
|
15
15
|
import { useTranslation } from 'react-i18next';
|
|
16
|
-
import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|
16
|
+
import { generatePath, Navigate, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
17
|
+
import { getModernClientPrefix } from '../authRedirect';
|
|
17
18
|
import type { PluginSettingsPageType } from '../PluginSettingsManager';
|
|
18
19
|
import { useApp } from '../hooks/useApp';
|
|
20
|
+
import { resolveSettingsAppScopeWithinPublicPath } from '../settings-app/settingsDocumentPath';
|
|
19
21
|
import { AdminSettingsLayoutModel } from './AdminSettingsLayoutModel';
|
|
20
22
|
import { useSettingsGroups } from './useSettingsGroups';
|
|
21
23
|
import {
|
|
@@ -34,6 +36,19 @@ import {
|
|
|
34
36
|
*/
|
|
35
37
|
const SETTINGS_CONTENT_MAX_WIDTH = 1280;
|
|
36
38
|
|
|
39
|
+
function AdminDocumentRedirect() {
|
|
40
|
+
const app = useApp();
|
|
41
|
+
const rootPublicPath = app.getPublicPath().replace(/\/+$/, '');
|
|
42
|
+
const appScope = resolveSettingsAppScopeWithinPublicPath(app.getPublicPath(), app.router.getBasename?.());
|
|
43
|
+
const targetPath = `${rootPublicPath}/${getModernClientPrefix()}${appScope}/admin`;
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
window.location.replace(targetPath);
|
|
47
|
+
}, [targetPath]);
|
|
48
|
+
|
|
49
|
+
return app.renderComponent('AppSpin');
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
function SettingsEmpty(props: { type: 'forbidden' | 'home' | 'not-found' }) {
|
|
38
53
|
const { type } = props;
|
|
39
54
|
const { t } = useTranslation();
|
|
@@ -83,6 +98,10 @@ export const InternalAdminSettingsLayout = () => {
|
|
|
83
98
|
const app = useApp();
|
|
84
99
|
const navigate = useNavigate();
|
|
85
100
|
const location = useLocation();
|
|
101
|
+
const params = useParams();
|
|
102
|
+
const routeParams = Object.fromEntries(
|
|
103
|
+
Object.entries(params).filter((entry): entry is [string, string] => typeof entry[1] === 'string'),
|
|
104
|
+
);
|
|
86
105
|
const { token } = theme.useToken();
|
|
87
106
|
const {
|
|
88
107
|
allSettings,
|
|
@@ -173,8 +192,24 @@ export const InternalAdminSettingsLayout = () => {
|
|
|
173
192
|
}
|
|
174
193
|
|
|
175
194
|
if (currentSetting.isAllow === false) {
|
|
176
|
-
|
|
177
|
-
|
|
195
|
+
const firstVisibleTabPath = (currentVisibleTopLevelSetting?.children || [])
|
|
196
|
+
.map((tab) => getDefaultSettingsPath([tab]))
|
|
197
|
+
.filter((path): path is string => typeof path === 'string')
|
|
198
|
+
.map((path) => {
|
|
199
|
+
try {
|
|
200
|
+
return generatePath(path, routeParams);
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
.find((path): path is string => typeof path === 'string');
|
|
206
|
+
|
|
207
|
+
if (firstVisibleTabPath) {
|
|
208
|
+
return <Navigate replace to={firstVisibleTabPath} />;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!defaultSettingsPath) {
|
|
212
|
+
return <AdminDocumentRedirect />;
|
|
178
213
|
}
|
|
179
214
|
|
|
180
215
|
return <SettingsEmpty type="forbidden" />;
|