@nocobase/client-v2 2.1.0-beta.27 → 2.1.0-beta.30
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/components/form/JsonTextArea.d.ts +18 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/dateRangeLimit.d.ts +9 -0
- package/es/flow/actions/index.d.ts +2 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +4 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +5 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +4 -0
- package/es/flow/models/base/PageModel/RootPageModel.d.ts +9 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +7 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +5 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +20 -0
- package/es/flow/models/fields/JSEditableFieldModel.d.ts +4 -0
- package/es/index.mjs +79 -67
- package/lib/index.js +80 -68
- package/package.json +6 -5
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +67 -46
- package/src/__tests__/settings-center.test.tsx +30 -0
- package/src/components/form/JsonTextArea.tsx +129 -0
- package/src/components/index.ts +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +4 -5
- package/src/flow/actions/__tests__/actionLinkageRules.race.repro.test.ts +199 -0
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +478 -0
- package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +6 -1
- package/src/flow/actions/__tests__/linkageRules.menu.test.ts +90 -0
- package/src/flow/actions/__tests__/pattern.test.ts +190 -0
- package/src/flow/actions/dateRangeLimit.tsx +66 -0
- package/src/flow/actions/index.ts +3 -0
- package/src/flow/actions/linkageRules.tsx +194 -42
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +2 -8
- package/src/flow/actions/openView.tsx +2 -1
- package/src/flow/actions/pattern.tsx +25 -2
- package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +7 -1
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +117 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +8 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +70 -12
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +26 -87
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +11 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +5 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +292 -31
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +50 -12
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +77 -56
- package/src/flow/components/AdminLayout.tsx +2 -2
- package/src/flow/components/FlowRoute.tsx +17 -4
- package/src/flow/models/base/PageModel/PageModel.tsx +15 -3
- package/src/flow/models/base/PageModel/RootPageModel.tsx +37 -2
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +73 -0
- package/src/flow/models/base/PageModel/__tests__/RootPageModel.test.ts +116 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +167 -1
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +103 -11
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +34 -3
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +47 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +42 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.refresh.test.tsx +122 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -1
- package/src/flow/models/fields/ClickableFieldModel.tsx +21 -9
- package/src/flow/models/fields/DateTimeFieldModel/DateOnlyFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeFieldModel.tsx +4 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeNoTzFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeTzFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +242 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +152 -0
- package/src/flow/models/fields/JSEditableFieldModel.tsx +110 -14
- package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +87 -0
- package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +210 -0
- package/src/flow/system-settings/useSystemSettings.tsx +36 -1
|
@@ -0,0 +1,122 @@
|
|
|
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 React from 'react';
|
|
11
|
+
import { EventEmitter } from 'events';
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import { act, render, screen } from '@nocobase/test/client';
|
|
14
|
+
import { SubTableField } from '../SubTableField';
|
|
15
|
+
|
|
16
|
+
vi.mock('react-i18next', async (importOriginal) => ({
|
|
17
|
+
...(await importOriginal<any>()),
|
|
18
|
+
useTranslation: () => ({
|
|
19
|
+
t: (value: string) => value,
|
|
20
|
+
}),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock('antd', async () => {
|
|
24
|
+
const actual = await vi.importActual<any>('antd');
|
|
25
|
+
return {
|
|
26
|
+
...actual,
|
|
27
|
+
Table: ({ dataSource = [], columns = [] }: any) => (
|
|
28
|
+
<div data-testid="subtable">
|
|
29
|
+
{dataSource.map((record: any, rowIdx: number) => (
|
|
30
|
+
<div data-testid={`row-${rowIdx}`} key={record.__index__ || rowIdx}>
|
|
31
|
+
{columns.map((column: any) => (
|
|
32
|
+
<div data-testid={`cell-${rowIdx}-${String(column.dataIndex || column.key)}`} key={column.key}>
|
|
33
|
+
{column.render?.(record[column.dataIndex], record, rowIdx)}
|
|
34
|
+
</div>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('SubTableField refresh', () => {
|
|
44
|
+
it('rerenders from current form value when a nested subtable path changes', () => {
|
|
45
|
+
const emitter = new EventEmitter();
|
|
46
|
+
const store = {
|
|
47
|
+
roles: [{ __is_new__: true, __index__: 'row-1', uid: 'role-uid-1', name: '' }],
|
|
48
|
+
};
|
|
49
|
+
const columns = [
|
|
50
|
+
{
|
|
51
|
+
key: 'name',
|
|
52
|
+
dataIndex: 'name',
|
|
53
|
+
render: ({ value }: any) => <span>{value || 'empty'}</span>,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
render(
|
|
58
|
+
<SubTableField
|
|
59
|
+
columns={columns}
|
|
60
|
+
pageSize={10}
|
|
61
|
+
filterTargetKey="id"
|
|
62
|
+
fieldPathArray={['roles']}
|
|
63
|
+
formValuesChangeEmitter={emitter}
|
|
64
|
+
getCurrentValue={() => store.roles}
|
|
65
|
+
/>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(screen.getByTestId('cell-0-name')).toHaveTextContent('empty');
|
|
69
|
+
|
|
70
|
+
act(() => {
|
|
71
|
+
store.roles = [{ ...store.roles[0], name: 'role-uid-1' }];
|
|
72
|
+
emitter.emit('formValuesChange', {
|
|
73
|
+
source: 'linkage',
|
|
74
|
+
changedPaths: [['roles', 0, 'name']],
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(screen.getByTestId('cell-0-name')).toHaveTextContent('role-uid-1');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('ignores unrelated form value changes', () => {
|
|
82
|
+
const emitter = new EventEmitter();
|
|
83
|
+
let renderCount = 0;
|
|
84
|
+
const store = {
|
|
85
|
+
roles: [{ __is_new__: true, __index__: 'row-1', uid: 'role-uid-1', name: '' }],
|
|
86
|
+
};
|
|
87
|
+
const columns = [
|
|
88
|
+
{
|
|
89
|
+
key: 'name',
|
|
90
|
+
dataIndex: 'name',
|
|
91
|
+
render: ({ value }: any) => {
|
|
92
|
+
renderCount += 1;
|
|
93
|
+
return <span>{value || 'empty'}</span>;
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
render(
|
|
99
|
+
<SubTableField
|
|
100
|
+
columns={columns}
|
|
101
|
+
pageSize={10}
|
|
102
|
+
filterTargetKey="id"
|
|
103
|
+
fieldPathArray={['roles']}
|
|
104
|
+
formValuesChangeEmitter={emitter}
|
|
105
|
+
getCurrentValue={() => store.roles}
|
|
106
|
+
/>,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(renderCount).toBe(1);
|
|
110
|
+
|
|
111
|
+
act(() => {
|
|
112
|
+
store.roles = [{ ...store.roles[0], name: 'role-uid-1' }];
|
|
113
|
+
emitter.emit('formValuesChange', {
|
|
114
|
+
source: 'user',
|
|
115
|
+
changedPaths: [['profile', 'name']],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(renderCount).toBe(1);
|
|
120
|
+
expect(screen.getByTestId('cell-0-name')).toHaveTextContent('empty');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -21,7 +21,7 @@ import { uid } from '@formily/shared';
|
|
|
21
21
|
import { FormItemModel } from '../../../blocks/form';
|
|
22
22
|
import { AssociationFieldModel } from '../AssociationFieldModel';
|
|
23
23
|
import { buildRecordPickerPopupContextInputArgs, RecordPickerContent } from '../RecordPickerFieldModel';
|
|
24
|
-
import { SubTableColumnModel } from './SubTableColumnModel';
|
|
24
|
+
import { isSubTableColumnFieldComponentContext, SubTableColumnModel } from './SubTableColumnModel';
|
|
25
25
|
import { SubTableField } from './SubTableField';
|
|
26
26
|
import { adjustColumnOrder } from '../../../blocks/table/utils';
|
|
27
27
|
|
|
@@ -120,6 +120,8 @@ export class SubTableFieldModel extends AssociationFieldModel {
|
|
|
120
120
|
parentFieldIndex={this.context.fieldIndex}
|
|
121
121
|
parentItem={this.context.item}
|
|
122
122
|
filterTargetKey={this.collection.filterTargetKey}
|
|
123
|
+
formValuesChangeEmitter={this.context.blockModel?.emitter}
|
|
124
|
+
fieldPathArray={this.parent?.context?.fieldPathArray}
|
|
123
125
|
getCurrentValue={this.getCurrentValue}
|
|
124
126
|
/>
|
|
125
127
|
);
|
|
@@ -387,6 +389,9 @@ export { SubTableColumnModel };
|
|
|
387
389
|
FormItemModel.bindModelToInterface('SubTableFieldModel', ['m2m', 'o2m', 'mbm'], {
|
|
388
390
|
order: 200,
|
|
389
391
|
when: (ctx, field) => {
|
|
392
|
+
if (isSubTableColumnFieldComponentContext(ctx)) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
390
395
|
if (field.targetCollection) {
|
|
391
396
|
return field.targetCollection.template !== 'file';
|
|
392
397
|
}
|
|
@@ -11,10 +11,9 @@ import { CollectionField, tExpr } from '@nocobase/flow-engine';
|
|
|
11
11
|
import { Tag } from 'antd';
|
|
12
12
|
import { castArray, get } from 'lodash';
|
|
13
13
|
import React from 'react';
|
|
14
|
-
import { EllipsisWithTooltip } from '../../components';
|
|
14
|
+
import { EllipsisWithTooltip } from '../../components/EllipsisWithTooltip';
|
|
15
15
|
import { openViewFlow } from '../../flows/openViewFlow';
|
|
16
16
|
import { FieldModel } from '../base/FieldModel';
|
|
17
|
-
import { EditFormModel } from '../blocks/form/EditFormModel';
|
|
18
17
|
|
|
19
18
|
export function transformNestedData(inputData) {
|
|
20
19
|
const resultArray = [];
|
|
@@ -36,6 +35,8 @@ export function transformNestedData(inputData) {
|
|
|
36
35
|
const hasAssociationPathName = (parent: unknown): parent is { associationPathName?: string } =>
|
|
37
36
|
!!parent && typeof parent === 'object' && 'associationPathName' in parent;
|
|
38
37
|
|
|
38
|
+
const hasUsableSourceId = (sourceId: unknown) => sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
|
|
39
|
+
|
|
39
40
|
export class ClickableFieldModel extends FieldModel {
|
|
40
41
|
get collectionField(): CollectionField {
|
|
41
42
|
return this.context.collectionField;
|
|
@@ -62,14 +63,18 @@ export class ClickableFieldModel extends FieldModel {
|
|
|
62
63
|
const parentObj = associationPathName
|
|
63
64
|
? get(this.context.blockModel?.form?.getFieldsValue?.(true) || this.context.record, associationPathName)
|
|
64
65
|
: this.context.record;
|
|
66
|
+
const sourceId = parentObj?.[sourceKey];
|
|
67
|
+
const useAssociationResource = hasUsableSourceId(sourceId);
|
|
65
68
|
this.dispatchEvent(
|
|
66
69
|
'click',
|
|
67
70
|
{
|
|
68
71
|
event,
|
|
69
72
|
filterByTk,
|
|
70
|
-
collectionName:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
collectionName: useAssociationResource
|
|
74
|
+
? this.collectionField.collection.name
|
|
75
|
+
: targetCollection?.name || this.collectionField.target,
|
|
76
|
+
associationName: useAssociationResource ? `${sourceCollection.name}.${this.collectionField.name}` : null,
|
|
77
|
+
sourceId: useAssociationResource ? sourceId : null,
|
|
73
78
|
},
|
|
74
79
|
{
|
|
75
80
|
debounce: true,
|
|
@@ -95,6 +100,10 @@ export class ClickableFieldModel extends FieldModel {
|
|
|
95
100
|
const parentObj = associationPathName.includes('.')
|
|
96
101
|
? get(this.context.record, associationPathName.split('.')[0])
|
|
97
102
|
: this.context.record;
|
|
103
|
+
const sourceId = hasUsableSourceId(parentObj?.[sourceKey])
|
|
104
|
+
? parentObj?.[sourceKey]
|
|
105
|
+
: this.context.record?.[foreignKey];
|
|
106
|
+
const useAssociationResource = hasUsableSourceId(sourceId);
|
|
98
107
|
let filterByTk = associationRecord?.[targetKey];
|
|
99
108
|
if (associationField.interface === 'm2m') {
|
|
100
109
|
// also incorrect for v1
|
|
@@ -106,10 +115,13 @@ export class ClickableFieldModel extends FieldModel {
|
|
|
106
115
|
{
|
|
107
116
|
event,
|
|
108
117
|
filterByTk,
|
|
109
|
-
collectionName:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
collectionName: useAssociationResource
|
|
119
|
+
? this.collectionField.collection.name
|
|
120
|
+
: targetCollection?.name || associationField.target || this.collectionField.collection.name,
|
|
121
|
+
associationName: useAssociationResource
|
|
122
|
+
? `${associationField.collection.name}.${this.collectionField.name}`
|
|
123
|
+
: null,
|
|
124
|
+
sourceId: useAssociationResource ? sourceId : null,
|
|
113
125
|
},
|
|
114
126
|
{
|
|
115
127
|
debounce: true,
|
|
@@ -12,17 +12,26 @@ import { EditableItemModel, useFlowModelContext } from '@nocobase/flow-engine';
|
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { DateTimeFieldModel } from './DateTimeFieldModel';
|
|
14
14
|
import { MobileDatePicker } from '../mobile-components/MobileDatePicker';
|
|
15
|
+
import { useDateLimit } from './dateLimit';
|
|
15
16
|
|
|
16
17
|
export const DateOnlyPicker = (props) => {
|
|
17
18
|
const { value, format = 'YYYY-MM-DD', picker = 'date', showTime, ...rest } = props;
|
|
18
19
|
const parsedValue = value && dayjs(value).isValid() ? dayjs(value) : null;
|
|
19
20
|
const ctx = useFlowModelContext();
|
|
21
|
+
const { disabledDate, disabledTime, minDate, maxDate } = useDateLimit({
|
|
22
|
+
...props,
|
|
23
|
+
currentForm: ctx.model?.context?.form,
|
|
24
|
+
});
|
|
20
25
|
const componentProps = {
|
|
21
26
|
...rest,
|
|
22
27
|
value: parsedValue,
|
|
23
28
|
format,
|
|
24
29
|
picker,
|
|
25
30
|
showTime,
|
|
31
|
+
disabledDate,
|
|
32
|
+
disabledTime,
|
|
33
|
+
minDate,
|
|
34
|
+
maxDate,
|
|
26
35
|
onChange: (val: any) => {
|
|
27
36
|
const outputFormat = 'YYYY-MM-DD';
|
|
28
37
|
if (!val) {
|
|
@@ -12,17 +12,26 @@ import React from 'react';
|
|
|
12
12
|
import { EditableItemModel, useFlowModelContext } from '@nocobase/flow-engine';
|
|
13
13
|
import { DateTimeFieldModel } from './DateTimeFieldModel';
|
|
14
14
|
import { MobileDatePicker } from '../mobile-components/MobileDatePicker';
|
|
15
|
+
import { useDateLimit } from './dateLimit';
|
|
15
16
|
|
|
16
17
|
export const DateTimeNoTzPicker = (props) => {
|
|
17
18
|
const { value, format = 'YYYY-MM-DD HH:mm:ss', showTime, picker = 'date', onChange, ...rest } = props;
|
|
18
19
|
const parsedValue = value ? dayjs(value) : null;
|
|
19
20
|
const ctx = useFlowModelContext();
|
|
21
|
+
const { disabledDate, disabledTime, minDate, maxDate } = useDateLimit({
|
|
22
|
+
...props,
|
|
23
|
+
currentForm: ctx.model?.context?.form,
|
|
24
|
+
});
|
|
20
25
|
const componentProps = {
|
|
21
26
|
...rest,
|
|
22
27
|
value: parsedValue,
|
|
23
28
|
format,
|
|
24
29
|
picker,
|
|
25
30
|
showTime,
|
|
31
|
+
disabledDate,
|
|
32
|
+
disabledTime,
|
|
33
|
+
minDate,
|
|
34
|
+
maxDate,
|
|
26
35
|
onChange: (val: any) => {
|
|
27
36
|
if (!val) {
|
|
28
37
|
return onChange(val);
|
|
@@ -12,6 +12,7 @@ import React from 'react';
|
|
|
12
12
|
import { DateTimeFieldModel } from './DateTimeFieldModel';
|
|
13
13
|
import { MobileDatePicker } from '../mobile-components/MobileDatePicker';
|
|
14
14
|
import { DatePicker } from 'antd';
|
|
15
|
+
import { useDateLimit } from './dateLimit';
|
|
15
16
|
|
|
16
17
|
function parseToDate(value: string | Date | dayjs.Dayjs | undefined, format?: string): Date | undefined {
|
|
17
18
|
if (!value) return undefined;
|
|
@@ -49,12 +50,20 @@ function parseInitialValue(value: string | Date | undefined, format?: string): d
|
|
|
49
50
|
export const DateTimeTzPicker = (props) => {
|
|
50
51
|
const { value, format = 'YYYY-MM-DD HH:mm:ss', picker = 'date', showTime, ...rest } = props;
|
|
51
52
|
const ctx = useFlowModelContext();
|
|
53
|
+
const { disabledDate, disabledTime, minDate, maxDate } = useDateLimit({
|
|
54
|
+
...props,
|
|
55
|
+
currentForm: ctx.model?.context?.form,
|
|
56
|
+
});
|
|
52
57
|
const componentProps = {
|
|
53
58
|
...rest,
|
|
54
59
|
value: parseInitialValue(value, format),
|
|
55
60
|
format,
|
|
56
61
|
picker,
|
|
57
62
|
showTime,
|
|
63
|
+
disabledDate,
|
|
64
|
+
disabledTime,
|
|
65
|
+
minDate,
|
|
66
|
+
maxDate,
|
|
58
67
|
onChange: (val: any) => {
|
|
59
68
|
let result = parseToDate(val, format);
|
|
60
69
|
// Adjust to start of period for month/quarter/year pickers
|
|
@@ -0,0 +1,242 @@
|
|
|
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 React from 'react';
|
|
11
|
+
import { Form } from 'antd';
|
|
12
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import { render, waitFor } from '@nocobase/test/client';
|
|
14
|
+
import { dayjs } from '@nocobase/utils/client';
|
|
15
|
+
import { DateTimeNoTzPicker } from '../DateTimeNoTzFieldModel';
|
|
16
|
+
|
|
17
|
+
let capturedDatePickerProps: any;
|
|
18
|
+
let currentForm: any;
|
|
19
|
+
const mockResolveJsonTemplate = vi.fn();
|
|
20
|
+
|
|
21
|
+
vi.mock('@nocobase/flow-engine', async (importOriginal) => {
|
|
22
|
+
const actual = await importOriginal<typeof import('@nocobase/flow-engine')>();
|
|
23
|
+
return {
|
|
24
|
+
...actual,
|
|
25
|
+
useFlowModelContext: () => ({
|
|
26
|
+
isMobileLayout: false,
|
|
27
|
+
model: {
|
|
28
|
+
context: {
|
|
29
|
+
form: currentForm,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
useFlowContext: () => ({
|
|
34
|
+
resolveJsonTemplate: mockResolveJsonTemplate,
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
vi.mock('antd', async (importOriginal) => {
|
|
40
|
+
const actual = await importOriginal<typeof import('antd')>();
|
|
41
|
+
return {
|
|
42
|
+
...actual,
|
|
43
|
+
DatePicker: (props: any) => {
|
|
44
|
+
capturedDatePickerProps = props;
|
|
45
|
+
return <div data-testid="date-picker" />;
|
|
46
|
+
},
|
|
47
|
+
Form: actual.Form,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const TestWrapper = (props: any) => {
|
|
52
|
+
const [form] = Form.useForm();
|
|
53
|
+
currentForm = form;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Form form={form} initialValues={{ b: '2026-05-10 12:34:56' }}>
|
|
57
|
+
<DateTimeNoTzPicker {...props} />
|
|
58
|
+
</Form>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe('DateTimeNoTzPicker date range limit', () => {
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
currentForm = undefined;
|
|
65
|
+
capturedDatePickerProps = undefined;
|
|
66
|
+
mockResolveJsonTemplate.mockReset();
|
|
67
|
+
mockResolveJsonTemplate.mockImplementation(async (params) => ({
|
|
68
|
+
...params,
|
|
69
|
+
_maxDate: currentForm?.getFieldValue?.('b'),
|
|
70
|
+
}));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('applies maxDate from current form field b', async () => {
|
|
74
|
+
render(<TestWrapper picker="date" showTime _maxDate={'{{ $nForm.b }}'} onChange={vi.fn()} value={null} />);
|
|
75
|
+
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
expect(mockResolveJsonTemplate).toHaveBeenCalledWith({
|
|
78
|
+
_minDate: undefined,
|
|
79
|
+
_maxDate: '{{ $nForm.b }}',
|
|
80
|
+
});
|
|
81
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-11 00:00:00'))).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const timeConfig = capturedDatePickerProps?.disabledTime?.(dayjs('2026-05-10 00:00:00'));
|
|
85
|
+
expect(timeConfig?.disabledHours?.()).toContain(13);
|
|
86
|
+
expect(timeConfig?.disabledHours?.()).not.toContain(12);
|
|
87
|
+
expect(timeConfig?.disabledMinutes?.(12)).toContain(35);
|
|
88
|
+
expect(timeConfig?.disabledMinutes?.(12)).not.toContain(34);
|
|
89
|
+
expect(timeConfig?.disabledSeconds?.(12, 34)).toContain(57);
|
|
90
|
+
expect(timeConfig?.disabledSeconds?.(12, 34)).not.toContain(56);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('updates field a maxDate immediately when field b changes', async () => {
|
|
94
|
+
render(<TestWrapper picker="date" showTime _maxDate={'{{ $nForm.b }}'} onChange={vi.fn()} value={null} />);
|
|
95
|
+
|
|
96
|
+
await waitFor(() => {
|
|
97
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-11 00:00:00'))).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
currentForm.setFieldValue('b', '2026-05-12 08:09:10');
|
|
101
|
+
|
|
102
|
+
await waitFor(() => {
|
|
103
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-11 00:00:00'))).toBe(false);
|
|
104
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-13 00:00:00'))).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const timeConfig = capturedDatePickerProps?.disabledTime?.(dayjs('2026-05-12 00:00:00'));
|
|
108
|
+
expect(timeConfig?.disabledHours?.()).toContain(9);
|
|
109
|
+
expect(timeConfig?.disabledHours?.()).not.toContain(8);
|
|
110
|
+
expect(timeConfig?.disabledMinutes?.(8)).toContain(10);
|
|
111
|
+
expect(timeConfig?.disabledMinutes?.(8)).not.toContain(9);
|
|
112
|
+
expect(timeConfig?.disabledSeconds?.(8, 9)).toContain(11);
|
|
113
|
+
expect(timeConfig?.disabledSeconds?.(8, 9)).not.toContain(10);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('applies minDate from current form field b', async () => {
|
|
117
|
+
mockResolveJsonTemplate.mockImplementation(async (params) => ({
|
|
118
|
+
...params,
|
|
119
|
+
_minDate: currentForm?.getFieldValue?.('b'),
|
|
120
|
+
_maxDate: undefined,
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
render(<TestWrapper picker="date" showTime _minDate={'{{ $nForm.b }}'} onChange={vi.fn()} value={null} />);
|
|
124
|
+
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-09 00:00:00'))).toBe(true);
|
|
127
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-10 00:00:00'))).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const timeConfig = capturedDatePickerProps?.disabledTime?.(dayjs('2026-05-10 00:00:00'));
|
|
131
|
+
expect(timeConfig?.disabledHours?.()).toContain(11);
|
|
132
|
+
expect(timeConfig?.disabledHours?.()).not.toContain(12);
|
|
133
|
+
expect(timeConfig?.disabledMinutes?.(12)).toContain(33);
|
|
134
|
+
expect(timeConfig?.disabledMinutes?.(12)).not.toContain(34);
|
|
135
|
+
expect(timeConfig?.disabledSeconds?.(12, 34)).toContain(55);
|
|
136
|
+
expect(timeConfig?.disabledSeconds?.(12, 34)).not.toContain(56);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('applies both minDate and maxDate together', async () => {
|
|
140
|
+
mockResolveJsonTemplate.mockImplementation(async (params) => ({
|
|
141
|
+
...params,
|
|
142
|
+
_minDate: '2026-05-10 08:00:00',
|
|
143
|
+
_maxDate: '2026-05-12 18:30:40',
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
render(
|
|
147
|
+
<TestWrapper
|
|
148
|
+
picker="date"
|
|
149
|
+
showTime
|
|
150
|
+
_minDate={'{{ $nForm.min }}'}
|
|
151
|
+
_maxDate={'{{ $nForm.max }}'}
|
|
152
|
+
onChange={vi.fn()}
|
|
153
|
+
value={null}
|
|
154
|
+
/>,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
await waitFor(() => {
|
|
158
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-09 00:00:00'))).toBe(true);
|
|
159
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-13 00:00:00'))).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-11 00:00:00'))).toBe(false);
|
|
163
|
+
|
|
164
|
+
const minDayTimeConfig = capturedDatePickerProps?.disabledTime?.(dayjs('2026-05-10 00:00:00'));
|
|
165
|
+
expect(minDayTimeConfig?.disabledHours?.()).toContain(7);
|
|
166
|
+
expect(minDayTimeConfig?.disabledHours?.()).not.toContain(8);
|
|
167
|
+
|
|
168
|
+
const maxDayTimeConfig = capturedDatePickerProps?.disabledTime?.(dayjs('2026-05-12 00:00:00'));
|
|
169
|
+
expect(maxDayTimeConfig?.disabledHours?.()).toContain(19);
|
|
170
|
+
expect(maxDayTimeConfig?.disabledHours?.()).not.toContain(18);
|
|
171
|
+
expect(maxDayTimeConfig?.disabledMinutes?.(18)).toContain(31);
|
|
172
|
+
expect(maxDayTimeConfig?.disabledMinutes?.(18)).not.toContain(30);
|
|
173
|
+
expect(maxDayTimeConfig?.disabledSeconds?.(18, 30)).toContain(41);
|
|
174
|
+
expect(maxDayTimeConfig?.disabledSeconds?.(18, 30)).not.toContain(40);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('uses first minDate and last maxDate when resolved values are arrays', async () => {
|
|
178
|
+
mockResolveJsonTemplate.mockImplementation(async (params) => ({
|
|
179
|
+
...params,
|
|
180
|
+
_minDate: ['2026-05-10 08:00:00', '2026-05-11 09:00:00'],
|
|
181
|
+
_maxDate: ['2026-05-12 10:00:00', '2026-05-13 11:12:13'],
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
render(
|
|
185
|
+
<TestWrapper
|
|
186
|
+
picker="date"
|
|
187
|
+
showTime
|
|
188
|
+
_minDate={'{{ $nForm.min }}'}
|
|
189
|
+
_maxDate={'{{ $nForm.max }}'}
|
|
190
|
+
onChange={vi.fn()}
|
|
191
|
+
value={null}
|
|
192
|
+
/>,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
await waitFor(() => {
|
|
196
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-09 00:00:00'))).toBe(true);
|
|
197
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-14 00:00:00'))).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-10 00:00:00'))).toBe(false);
|
|
201
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-13 00:00:00'))).toBe(false);
|
|
202
|
+
|
|
203
|
+
const timeConfig = capturedDatePickerProps?.disabledTime?.(dayjs('2026-05-13 00:00:00'));
|
|
204
|
+
expect(timeConfig?.disabledHours?.()).toContain(12);
|
|
205
|
+
expect(timeConfig?.disabledHours?.()).not.toContain(11);
|
|
206
|
+
expect(timeConfig?.disabledMinutes?.(11)).toContain(13);
|
|
207
|
+
expect(timeConfig?.disabledMinutes?.(11)).not.toContain(12);
|
|
208
|
+
expect(timeConfig?.disabledSeconds?.(11, 12)).toContain(14);
|
|
209
|
+
expect(timeConfig?.disabledSeconds?.(11, 12)).not.toContain(13);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('clears date and time restrictions when resolved values are empty', async () => {
|
|
213
|
+
mockResolveJsonTemplate.mockImplementation(async (params) => ({
|
|
214
|
+
...params,
|
|
215
|
+
_minDate: undefined,
|
|
216
|
+
_maxDate: undefined,
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
render(
|
|
220
|
+
<TestWrapper
|
|
221
|
+
picker="date"
|
|
222
|
+
showTime
|
|
223
|
+
_minDate={'{{ $nForm.min }}'}
|
|
224
|
+
_maxDate={'{{ $nForm.max }}'}
|
|
225
|
+
onChange={vi.fn()}
|
|
226
|
+
value={null}
|
|
227
|
+
/>,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
await waitFor(() => {
|
|
231
|
+
expect(capturedDatePickerProps).toBeTruthy();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-01 00:00:00'))).toBe(false);
|
|
235
|
+
expect(capturedDatePickerProps?.disabledDate?.(dayjs('2026-05-31 00:00:00'))).toBe(false);
|
|
236
|
+
|
|
237
|
+
const timeConfig = capturedDatePickerProps?.disabledTime?.(dayjs('2026-05-15 00:00:00'));
|
|
238
|
+
expect(timeConfig?.disabledHours?.()).toEqual([]);
|
|
239
|
+
expect(timeConfig?.disabledMinutes?.(12)).toEqual([]);
|
|
240
|
+
expect(timeConfig?.disabledSeconds?.(12, 34)).toEqual([]);
|
|
241
|
+
});
|
|
242
|
+
});
|