@nocobase/client-v2 2.2.0-beta.2 → 2.2.0-beta.5
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/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
- package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
- package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
- package/es/flow/utils/formValueDeps.d.ts +32 -0
- package/es/index.mjs +113 -93
- package/lib/index.js +104 -84
- package/package.json +7 -7
- package/src/components/AppComponents.tsx +19 -3
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
- package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
- package/src/flow/actions/dateTimeFormat.tsx +42 -28
- package/src/flow/actions/linkageRules.tsx +1 -1
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
- package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
- package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
- package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
- package/src/flow/flows/editMarkdownFlow.tsx +1 -1
- package/src/flow/index.ts +1 -1
- package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
- package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
- package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
- package/src/flow/models/fields/InputFieldModel.tsx +48 -2
- package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
- package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
- package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +100 -1
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
- package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
- package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
- package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
- package/src/flow/utils/formValueDeps.ts +170 -0
- package/src/settings-center/AdminSettingsLayout.tsx +7 -2
|
@@ -8,15 +8,61 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Input } from 'antd';
|
|
11
|
-
import
|
|
12
|
-
import
|
|
11
|
+
import type { TextAreaProps } from 'antd/es/input';
|
|
12
|
+
import type { TextAreaRef } from 'antd/es/input/TextArea';
|
|
13
|
+
import React, { useEffect, useRef } from 'react';
|
|
13
14
|
import { largeField, EditableItemModel } from '@nocobase/flow-engine';
|
|
14
15
|
import { FieldModel } from '../base/FieldModel';
|
|
15
16
|
|
|
17
|
+
function IMESafeTextArea(props: TextAreaProps) {
|
|
18
|
+
const { value, onChange, onCompositionStart, onCompositionEnd, ...rest } = props;
|
|
19
|
+
const textAreaRef = useRef<TextAreaRef>(null);
|
|
20
|
+
const previousValueRef = useRef(value);
|
|
21
|
+
const defaultValue = typeof value === 'bigint' ? String(value) : value;
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (Object.is(previousValueRef.current, value)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
previousValueRef.current = value;
|
|
28
|
+
|
|
29
|
+
const textArea = textAreaRef.current?.resizableTextArea?.textArea;
|
|
30
|
+
if (textArea) {
|
|
31
|
+
const nextValue = value == null ? '' : String(value);
|
|
32
|
+
if (textArea.value !== nextValue) {
|
|
33
|
+
textArea.value = nextValue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}, [value]);
|
|
37
|
+
|
|
38
|
+
const getEventValue = (event: React.ChangeEvent<HTMLTextAreaElement> | React.CompositionEvent<HTMLTextAreaElement>) =>
|
|
39
|
+
event.currentTarget.value;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Input.TextArea
|
|
43
|
+
{...rest}
|
|
44
|
+
ref={textAreaRef}
|
|
45
|
+
defaultValue={defaultValue}
|
|
46
|
+
onChange={(event) => {
|
|
47
|
+
previousValueRef.current = getEventValue(event);
|
|
48
|
+
onChange?.(event);
|
|
49
|
+
}}
|
|
50
|
+
onCompositionStart={(event) => {
|
|
51
|
+
previousValueRef.current = getEventValue(event);
|
|
52
|
+
onCompositionStart?.(event);
|
|
53
|
+
}}
|
|
54
|
+
onCompositionEnd={(event) => {
|
|
55
|
+
previousValueRef.current = getEventValue(event);
|
|
56
|
+
onCompositionEnd?.(event);
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
16
62
|
@largeField()
|
|
17
63
|
export class TextareaFieldModel extends FieldModel {
|
|
18
64
|
render() {
|
|
19
|
-
return <
|
|
65
|
+
return <IMESafeTextArea {...this.props} />;
|
|
20
66
|
}
|
|
21
67
|
}
|
|
22
68
|
|
|
@@ -0,0 +1,96 @@
|
|
|
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 { FlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import { render, screen } from '@testing-library/react';
|
|
12
|
+
import { describe, expect, it } from 'vitest';
|
|
13
|
+
import { DisplayDateTimeFieldModel } from '../DisplayDateTimeFieldModel';
|
|
14
|
+
|
|
15
|
+
describe('DisplayDateTimeFieldModel', () => {
|
|
16
|
+
it('uses dateFormat, showTime, and timeFormat when rendering read pretty datetime text', () => {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
engine.registerModels({ DisplayDateTimeFieldModel });
|
|
19
|
+
|
|
20
|
+
const model = engine.createModel<DisplayDateTimeFieldModel>({
|
|
21
|
+
use: DisplayDateTimeFieldModel,
|
|
22
|
+
uid: 'display-datetime-field-format',
|
|
23
|
+
props: {
|
|
24
|
+
value: '2026-06-15 13:05:06',
|
|
25
|
+
dateFormat: 'YYYY-MM-DD',
|
|
26
|
+
showTime: true,
|
|
27
|
+
timeFormat: 'HH:mm:ss',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
render(model.render());
|
|
32
|
+
|
|
33
|
+
expect(screen.getByText('2026-06-15 13:05:06')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('does not reuse a time-only format when rendering datetime text', () => {
|
|
37
|
+
const engine = new FlowEngine();
|
|
38
|
+
engine.registerModels({ DisplayDateTimeFieldModel });
|
|
39
|
+
|
|
40
|
+
const model = engine.createModel<DisplayDateTimeFieldModel>({
|
|
41
|
+
use: DisplayDateTimeFieldModel,
|
|
42
|
+
uid: 'display-datetime-field-stale-time-format',
|
|
43
|
+
props: {
|
|
44
|
+
value: '2026-06-15 13:05:06',
|
|
45
|
+
format: 'HH:mm:ss',
|
|
46
|
+
showTime: true,
|
|
47
|
+
timeFormat: 'HH:mm:ss',
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
render(model.render());
|
|
52
|
+
|
|
53
|
+
expect(screen.getByText('2026-06-15 13:05:06')).toBeInTheDocument();
|
|
54
|
+
expect(screen.queryByText('13:05:06')).not.toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('keeps an existing complete datetime format when no split format props are configured', () => {
|
|
58
|
+
const engine = new FlowEngine();
|
|
59
|
+
engine.registerModels({ DisplayDateTimeFieldModel });
|
|
60
|
+
|
|
61
|
+
const model = engine.createModel<DisplayDateTimeFieldModel>({
|
|
62
|
+
use: DisplayDateTimeFieldModel,
|
|
63
|
+
uid: 'display-datetime-field-complete-format',
|
|
64
|
+
props: {
|
|
65
|
+
value: '2026-06-15 13:05:06',
|
|
66
|
+
format: 'YYYY/MM/DD HH:mm:ss',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
render(model.render());
|
|
71
|
+
|
|
72
|
+
expect(screen.getByText('2026/06/15 13:05:06')).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('renders only the date for date-only fields even when a datetime format remains', () => {
|
|
76
|
+
const engine = new FlowEngine();
|
|
77
|
+
engine.registerModels({ DisplayDateTimeFieldModel });
|
|
78
|
+
|
|
79
|
+
const model = engine.createModel<DisplayDateTimeFieldModel>({
|
|
80
|
+
use: DisplayDateTimeFieldModel,
|
|
81
|
+
uid: 'display-datetime-field-date-only',
|
|
82
|
+
props: {
|
|
83
|
+
value: '2026-06-15 13:05:06',
|
|
84
|
+
dateOnly: true,
|
|
85
|
+
format: 'YYYY-MM-DD HH:mm:ss',
|
|
86
|
+
showTime: false,
|
|
87
|
+
timeFormat: 'HH:mm:ss',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
render(model.render());
|
|
92
|
+
|
|
93
|
+
expect(screen.getByText('2026-06-15')).toBeInTheDocument();
|
|
94
|
+
expect(screen.queryByText('2026-06-15 13:05:06')).not.toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { FlowEngine } from '@nocobase/flow-engine';
|
|
11
|
-
import { render, screen } from '@testing-library/react';
|
|
11
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { describe, expect, it, vi } from 'vitest';
|
|
14
14
|
import { InputFieldModel } from '../InputFieldModel';
|
|
@@ -43,6 +43,105 @@ describe('InputFieldModel', () => {
|
|
|
43
43
|
consoleError.mockRestore();
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
it('keeps IME composition text visible before the parent value is committed', () => {
|
|
47
|
+
const onChange = vi.fn();
|
|
48
|
+
const onCompositionStart = vi.fn();
|
|
49
|
+
const onCompositionEnd = vi.fn();
|
|
50
|
+
const model = createInputFieldModel({
|
|
51
|
+
value: '',
|
|
52
|
+
onChange,
|
|
53
|
+
onCompositionStart,
|
|
54
|
+
onCompositionEnd,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
render(<>{model.render()}</>);
|
|
58
|
+
|
|
59
|
+
const input = screen.getByRole('textbox') as HTMLInputElement;
|
|
60
|
+
|
|
61
|
+
fireEvent.compositionStart(input);
|
|
62
|
+
fireEvent.change(input, { target: { value: 'ni' }, nativeEvent: { isComposing: true } });
|
|
63
|
+
|
|
64
|
+
expect(input.value).toBe('ni');
|
|
65
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(onCompositionStart).toHaveBeenCalledTimes(1);
|
|
67
|
+
|
|
68
|
+
fireEvent.compositionEnd(input, { target: { value: '你' } });
|
|
69
|
+
|
|
70
|
+
expect(input.value).toBe('你');
|
|
71
|
+
expect(onCompositionEnd).toHaveBeenCalledTimes(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns to the committed parent value after IME composition is submitted', () => {
|
|
75
|
+
const onChange = vi.fn();
|
|
76
|
+
const onCompositionEnd = vi.fn();
|
|
77
|
+
const createModel = (value: string) =>
|
|
78
|
+
createInputFieldModel({
|
|
79
|
+
value,
|
|
80
|
+
onChange,
|
|
81
|
+
onCompositionEnd,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const { rerender } = render(<>{createModel('').render()}</>);
|
|
85
|
+
const input = screen.getByRole('textbox') as HTMLInputElement;
|
|
86
|
+
|
|
87
|
+
fireEvent.compositionStart(input);
|
|
88
|
+
fireEvent.change(input, { target: { value: 'ni' }, nativeEvent: { isComposing: true } });
|
|
89
|
+
fireEvent.compositionEnd(input, { target: { value: '你' } });
|
|
90
|
+
|
|
91
|
+
expect(input.value).toBe('你');
|
|
92
|
+
|
|
93
|
+
rerender(<>{createModel('你').render()}</>);
|
|
94
|
+
|
|
95
|
+
expect(input.value).toBe('你');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('does not overwrite in-progress DOM input with a stale parent value', async () => {
|
|
99
|
+
const onChange = vi.fn();
|
|
100
|
+
const createModel = (value: string) =>
|
|
101
|
+
createInputFieldModel({
|
|
102
|
+
value,
|
|
103
|
+
onChange,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const { rerender } = render(<>{createModel('').render()}</>);
|
|
107
|
+
const input = screen.getByRole('textbox') as HTMLInputElement;
|
|
108
|
+
|
|
109
|
+
fireEvent.change(input, { target: { value: 'té' } });
|
|
110
|
+
rerender(<>{createModel('').render()}</>);
|
|
111
|
+
|
|
112
|
+
expect(input.value).toBe('té');
|
|
113
|
+
|
|
114
|
+
rerender(<>{createModel('reset').render()}</>);
|
|
115
|
+
|
|
116
|
+
await waitFor(() => {
|
|
117
|
+
expect((screen.getByRole('textbox') as HTMLInputElement).value).toBe('reset');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('keeps replacement-style Vietnamese IME edits in the DOM', () => {
|
|
122
|
+
const onChange = vi.fn();
|
|
123
|
+
const createModel = (value: string) =>
|
|
124
|
+
createInputFieldModel({
|
|
125
|
+
value,
|
|
126
|
+
onChange,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const { rerender } = render(<>{createModel('').render()}</>);
|
|
130
|
+
const input = screen.getByRole('textbox') as HTMLInputElement;
|
|
131
|
+
|
|
132
|
+
fireEvent.change(input, { target: { value: 'te' } });
|
|
133
|
+
rerender(<>{createModel('te').render()}</>);
|
|
134
|
+
expect(input.value).toBe('te');
|
|
135
|
+
|
|
136
|
+
fireEvent.change(input, { target: { value: 'té' } });
|
|
137
|
+
rerender(<>{createModel('te').render()}</>);
|
|
138
|
+
expect(input.value).toBe('té');
|
|
139
|
+
|
|
140
|
+
fireEvent.change(input, { target: { value: 'tét' } });
|
|
141
|
+
rerender(<>{createModel('té').render()}</>);
|
|
142
|
+
expect(input.value).toBe('tét');
|
|
143
|
+
});
|
|
144
|
+
|
|
46
145
|
it('renders ScanInput when scan input is enabled', () => {
|
|
47
146
|
const model = createInputFieldModel({ enableScan: true });
|
|
48
147
|
|
|
@@ -0,0 +1,43 @@
|
|
|
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 { describe, expect, it } from 'vitest';
|
|
12
|
+
import { SelectFieldModel } from '../SelectFieldModel';
|
|
13
|
+
|
|
14
|
+
function mockT(text: string) {
|
|
15
|
+
if (text === '{{t("Yes")}}') return '是';
|
|
16
|
+
if (text === '{{t("No")}}') return '否';
|
|
17
|
+
return text;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('SelectFieldModel', () => {
|
|
21
|
+
it('translates enum fallback labels for selected values', () => {
|
|
22
|
+
const model = {
|
|
23
|
+
props: {
|
|
24
|
+
value: true,
|
|
25
|
+
},
|
|
26
|
+
context: {
|
|
27
|
+
collectionField: {
|
|
28
|
+
uiSchema: {
|
|
29
|
+
enum: [
|
|
30
|
+
{ label: '{{t("Yes")}}', value: true },
|
|
31
|
+
{ label: '{{t("No")}}', value: false },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
translate: mockT,
|
|
37
|
+
} as unknown as SelectFieldModel;
|
|
38
|
+
|
|
39
|
+
const element = SelectFieldModel.prototype.render.call(model) as React.ReactElement;
|
|
40
|
+
|
|
41
|
+
expect(element.props.value).toEqual({ label: '是', value: true });
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
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 { FlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import { TextareaFieldModel } from '../TextareaFieldModel';
|
|
15
|
+
|
|
16
|
+
function createTextareaFieldModel(props?: Record<string, unknown>) {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
return new TextareaFieldModel({
|
|
19
|
+
uid: 'textarea-field-model-test',
|
|
20
|
+
flowEngine: engine,
|
|
21
|
+
props,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('TextareaFieldModel', () => {
|
|
26
|
+
it('keeps IME composition text visible before the parent value is committed', () => {
|
|
27
|
+
const onChange = vi.fn();
|
|
28
|
+
const onCompositionStart = vi.fn();
|
|
29
|
+
const onCompositionEnd = vi.fn();
|
|
30
|
+
const model = createTextareaFieldModel({
|
|
31
|
+
value: '',
|
|
32
|
+
onChange,
|
|
33
|
+
onCompositionStart,
|
|
34
|
+
onCompositionEnd,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
render(<>{model.render()}</>);
|
|
38
|
+
|
|
39
|
+
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
|
40
|
+
|
|
41
|
+
fireEvent.compositionStart(textarea);
|
|
42
|
+
fireEvent.change(textarea, { target: { value: 'ni' }, nativeEvent: { isComposing: true } });
|
|
43
|
+
|
|
44
|
+
expect(textarea.value).toBe('ni');
|
|
45
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
46
|
+
expect(onCompositionStart).toHaveBeenCalledTimes(1);
|
|
47
|
+
|
|
48
|
+
fireEvent.compositionEnd(textarea, { target: { value: '你' } });
|
|
49
|
+
|
|
50
|
+
expect(textarea.value).toBe('你');
|
|
51
|
+
expect(onCompositionEnd).toHaveBeenCalledTimes(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does not overwrite in-progress DOM input with a stale parent value', async () => {
|
|
55
|
+
const onChange = vi.fn();
|
|
56
|
+
const createModel = (value: string) =>
|
|
57
|
+
createTextareaFieldModel({
|
|
58
|
+
value,
|
|
59
|
+
onChange,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const { rerender } = render(<>{createModel('').render()}</>);
|
|
63
|
+
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
|
64
|
+
|
|
65
|
+
fireEvent.change(textarea, { target: { value: 'té' } });
|
|
66
|
+
rerender(<>{createModel('').render()}</>);
|
|
67
|
+
|
|
68
|
+
expect(textarea.value).toBe('té');
|
|
69
|
+
|
|
70
|
+
rerender(<>{createModel('reset').render()}</>);
|
|
71
|
+
|
|
72
|
+
await waitFor(() => {
|
|
73
|
+
expect((screen.getByRole('textbox') as HTMLTextAreaElement).value).toBe('reset');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('keeps replacement-style Vietnamese IME edits in the DOM', () => {
|
|
78
|
+
const onChange = vi.fn();
|
|
79
|
+
const createModel = (value: string) =>
|
|
80
|
+
createTextareaFieldModel({
|
|
81
|
+
value,
|
|
82
|
+
onChange,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const { rerender } = render(<>{createModel('').render()}</>);
|
|
86
|
+
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
|
87
|
+
|
|
88
|
+
fireEvent.change(textarea, { target: { value: 'te' } });
|
|
89
|
+
rerender(<>{createModel('te').render()}</>);
|
|
90
|
+
expect(textarea.value).toBe('te');
|
|
91
|
+
|
|
92
|
+
fireEvent.change(textarea, { target: { value: 'té' } });
|
|
93
|
+
rerender(<>{createModel('te').render()}</>);
|
|
94
|
+
expect(textarea.value).toBe('té');
|
|
95
|
+
|
|
96
|
+
fireEvent.change(textarea, { target: { value: 'tét' } });
|
|
97
|
+
rerender(<>{createModel('té').render()}</>);
|
|
98
|
+
expect(textarea.value).toBe('tét');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -48,6 +48,264 @@ describe('dateTimeFormat', () => {
|
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
it('uses the association title field when deciding the format schema', () => {
|
|
52
|
+
const ctx = {
|
|
53
|
+
model: {
|
|
54
|
+
props: {
|
|
55
|
+
titleField: 'shipmentsTime',
|
|
56
|
+
},
|
|
57
|
+
context: {
|
|
58
|
+
collectionField: {
|
|
59
|
+
type: 'belongsTo',
|
|
60
|
+
targetCollection: {
|
|
61
|
+
getField: (name) =>
|
|
62
|
+
name === 'shipmentsTime'
|
|
63
|
+
? {
|
|
64
|
+
type: 'time',
|
|
65
|
+
interface: 'time',
|
|
66
|
+
}
|
|
67
|
+
: null,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
expect(Object.keys(dateTimeFormat.uiSchema(ctx))).toEqual(['timeFormat']);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('saves association title time field format as a time format', () => {
|
|
78
|
+
const setProps = vi.fn();
|
|
79
|
+
const ctx = {
|
|
80
|
+
model: {
|
|
81
|
+
props: {
|
|
82
|
+
titleField: 'shipmentsTime',
|
|
83
|
+
},
|
|
84
|
+
context: {
|
|
85
|
+
collectionField: {
|
|
86
|
+
type: 'belongsTo',
|
|
87
|
+
targetCollection: {
|
|
88
|
+
getField: (name) =>
|
|
89
|
+
name === 'shipmentsTime'
|
|
90
|
+
? {
|
|
91
|
+
type: 'time',
|
|
92
|
+
interface: 'time',
|
|
93
|
+
}
|
|
94
|
+
: null,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
setProps,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
dateTimeFormat.handler(ctx, { timeFormat: 'hh:mm:ss a' });
|
|
103
|
+
|
|
104
|
+
expect(setProps).toHaveBeenCalledWith({
|
|
105
|
+
timeFormat: 'hh:mm:ss a',
|
|
106
|
+
format: 'hh:mm:ss a',
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('applies and persists date time format params when settings are saved', async () => {
|
|
111
|
+
const setProps = vi.fn();
|
|
112
|
+
const save = vi.fn();
|
|
113
|
+
const ctx = {
|
|
114
|
+
model: {
|
|
115
|
+
props: {
|
|
116
|
+
titleField: 'shipmentsDatetime',
|
|
117
|
+
},
|
|
118
|
+
context: {
|
|
119
|
+
collectionField: {
|
|
120
|
+
type: 'belongsTo',
|
|
121
|
+
targetCollection: {
|
|
122
|
+
getField: (name) =>
|
|
123
|
+
name === 'shipmentsDatetime'
|
|
124
|
+
? {
|
|
125
|
+
type: 'datetime',
|
|
126
|
+
interface: 'datetime',
|
|
127
|
+
}
|
|
128
|
+
: null,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
setProps,
|
|
133
|
+
save,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
await dateTimeFormat.beforeParamsSave(ctx, {
|
|
138
|
+
picker: 'date',
|
|
139
|
+
dateFormat: 'YYYY-MM-DD',
|
|
140
|
+
showTime: true,
|
|
141
|
+
timeFormat: 'hh:mm:ss a',
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(setProps).toHaveBeenCalledWith({
|
|
145
|
+
picker: 'date',
|
|
146
|
+
dateFormat: 'YYYY-MM-DD',
|
|
147
|
+
showTime: true,
|
|
148
|
+
timeFormat: 'hh:mm:ss a',
|
|
149
|
+
format: 'YYYY-MM-DD hh:mm:ss a',
|
|
150
|
+
});
|
|
151
|
+
expect(save).toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('syncs table association column props when title date time format settings are saved', async () => {
|
|
155
|
+
const setProps = vi.fn();
|
|
156
|
+
const save = vi.fn();
|
|
157
|
+
const setParentProps = vi.fn();
|
|
158
|
+
const model = {
|
|
159
|
+
props: {
|
|
160
|
+
titleField: 'shipmentsDatetime',
|
|
161
|
+
},
|
|
162
|
+
context: {
|
|
163
|
+
collectionField: {
|
|
164
|
+
type: 'belongsTo',
|
|
165
|
+
targetCollection: {
|
|
166
|
+
getField: (name) =>
|
|
167
|
+
name === 'shipmentsDatetime'
|
|
168
|
+
? {
|
|
169
|
+
type: 'datetime',
|
|
170
|
+
interface: 'datetime',
|
|
171
|
+
}
|
|
172
|
+
: null,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
setProps,
|
|
177
|
+
save,
|
|
178
|
+
parent: {
|
|
179
|
+
use: 'TableColumnModel',
|
|
180
|
+
collectionField: {
|
|
181
|
+
isAssociationField: () => true,
|
|
182
|
+
},
|
|
183
|
+
setProps: setParentProps,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
model.parent['subModels'] = {
|
|
187
|
+
field: model,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
await dateTimeFormat.beforeParamsSave(
|
|
191
|
+
{ model },
|
|
192
|
+
{
|
|
193
|
+
picker: 'date',
|
|
194
|
+
dateFormat: 'YYYY-MM-DD',
|
|
195
|
+
showTime: true,
|
|
196
|
+
timeFormat: 'hh:mm:ss a',
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(setParentProps).toHaveBeenCalledWith({
|
|
201
|
+
picker: 'date',
|
|
202
|
+
dateFormat: 'YYYY-MM-DD',
|
|
203
|
+
showTime: true,
|
|
204
|
+
timeFormat: 'hh:mm:ss a',
|
|
205
|
+
format: 'YYYY-MM-DD hh:mm:ss a',
|
|
206
|
+
});
|
|
207
|
+
expect(save).toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('hides time format for association title date-only fields', () => {
|
|
211
|
+
const ctx = {
|
|
212
|
+
model: {
|
|
213
|
+
props: {
|
|
214
|
+
titleField: 'shipmentsDateOnly',
|
|
215
|
+
showTime: true,
|
|
216
|
+
},
|
|
217
|
+
context: {
|
|
218
|
+
collectionField: {
|
|
219
|
+
type: 'belongsTo',
|
|
220
|
+
targetCollection: {
|
|
221
|
+
getField: (name) =>
|
|
222
|
+
name === 'shipmentsDateOnly'
|
|
223
|
+
? {
|
|
224
|
+
type: 'dateOnly',
|
|
225
|
+
interface: 'date',
|
|
226
|
+
getComponentProps: () => ({
|
|
227
|
+
dateOnly: true,
|
|
228
|
+
showTime: false,
|
|
229
|
+
}),
|
|
230
|
+
}
|
|
231
|
+
: null,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
const schema = dateTimeFormat.uiSchema(ctx);
|
|
238
|
+
const timeFormatField: any = schema.timeFormat;
|
|
239
|
+
const showTimeField: any = schema.showTime;
|
|
240
|
+
const timeFormatState = {
|
|
241
|
+
hidden: false,
|
|
242
|
+
form: {
|
|
243
|
+
values: {
|
|
244
|
+
picker: 'date',
|
|
245
|
+
showTime: true,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
const showTimeState = {
|
|
250
|
+
hidden: false,
|
|
251
|
+
value: true,
|
|
252
|
+
form: {
|
|
253
|
+
values: {
|
|
254
|
+
picker: 'date',
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
timeFormatField['x-reactions'][0](timeFormatState);
|
|
260
|
+
showTimeField['x-reactions'][1](showTimeState);
|
|
261
|
+
|
|
262
|
+
expect(timeFormatState.hidden).toBe(true);
|
|
263
|
+
expect(showTimeState.hidden).toBe(true);
|
|
264
|
+
expect(showTimeState.value).toBe(false);
|
|
265
|
+
expect(dateTimeFormat.defaultParams(ctx)).toMatchObject({
|
|
266
|
+
showTime: false,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('saves association title date-only field format without time even when params contain showTime', () => {
|
|
271
|
+
const setProps = vi.fn();
|
|
272
|
+
const ctx = {
|
|
273
|
+
model: {
|
|
274
|
+
props: {
|
|
275
|
+
titleField: 'shipmentsDateOnly',
|
|
276
|
+
},
|
|
277
|
+
context: {
|
|
278
|
+
collectionField: {
|
|
279
|
+
type: 'belongsTo',
|
|
280
|
+
targetCollection: {
|
|
281
|
+
getField: (name) =>
|
|
282
|
+
name === 'shipmentsDateOnly'
|
|
283
|
+
? {
|
|
284
|
+
type: 'dateOnly',
|
|
285
|
+
interface: 'date',
|
|
286
|
+
}
|
|
287
|
+
: null,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
setProps,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
dateTimeFormat.handler(ctx, {
|
|
296
|
+
dateFormat: 'YYYY-MM-DD',
|
|
297
|
+
showTime: true,
|
|
298
|
+
timeFormat: 'HH:mm:ss',
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect(setProps).toHaveBeenCalledWith({
|
|
302
|
+
dateFormat: 'YYYY-MM-DD',
|
|
303
|
+
showTime: false,
|
|
304
|
+
timeFormat: 'HH:mm:ss',
|
|
305
|
+
format: 'YYYY-MM-DD',
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
51
309
|
it('uses format as the default time format when timeFormat is missing', () => {
|
|
52
310
|
const ctx = {
|
|
53
311
|
model: {
|