@beinformed/ui 1.58.4 → 1.59.0
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/CHANGELOG.md +17 -0
- package/esm/constants/Constants.js +10 -0
- package/esm/constants/Constants.js.map +1 -1
- package/esm/constants/Settings.js +5 -1
- package/esm/constants/Settings.js.map +1 -1
- package/esm/models/attributes/DatetimeAttributeModel.js +39 -4
- package/esm/models/attributes/DatetimeAttributeModel.js.map +1 -1
- package/esm/models/attributes/input-constraints/DatetimeFormatConstraint.js +31 -2
- package/esm/models/attributes/input-constraints/DatetimeFormatConstraint.js.map +1 -1
- package/esm/react-client/client.js +2 -1
- package/esm/react-client/client.js.map +1 -1
- package/esm/react-server/serverUtil.js +2 -1
- package/esm/react-server/serverUtil.js.map +1 -1
- package/esm/redux/actions/Preferences.js +15 -1
- package/esm/redux/actions/Preferences.js.map +1 -1
- package/esm/utils/datetime/DateTimeUtil.js +292 -94
- package/esm/utils/datetime/DateTimeUtil.js.map +1 -1
- package/lib/constants/Constants.js +11 -1
- package/lib/constants/Constants.js.flow +11 -0
- package/lib/constants/Constants.js.map +1 -1
- package/lib/constants/Settings.js +7 -2
- package/lib/constants/Settings.js.flow +6 -0
- package/lib/constants/Settings.js.map +1 -1
- package/lib/models/attributes/DatetimeAttributeModel.js +38 -3
- package/lib/models/attributes/DatetimeAttributeModel.js.flow +54 -4
- package/lib/models/attributes/DatetimeAttributeModel.js.map +1 -1
- package/lib/models/attributes/__tests__/DatetimeAttributeModel.spec.js.flow +9 -0
- package/lib/models/attributes/__tests__/DatetimeAttributeModel_offset.spec.js.flow +306 -0
- package/lib/models/attributes/input-constraints/DatetimeFormatConstraint.js +31 -2
- package/lib/models/attributes/input-constraints/DatetimeFormatConstraint.js.flow +42 -3
- package/lib/models/attributes/input-constraints/DatetimeFormatConstraint.js.map +1 -1
- package/lib/react-client/client.js +1 -0
- package/lib/react-client/client.js.flow +2 -0
- package/lib/react-client/client.js.map +1 -1
- package/lib/react-server/__tests__/serverUtil.spec.js.flow +12 -0
- package/lib/react-server/serverUtil.js +1 -0
- package/lib/react-server/serverUtil.js.flow +2 -0
- package/lib/react-server/serverUtil.js.map +1 -1
- package/lib/redux/actions/Preferences.js +17 -2
- package/lib/redux/actions/Preferences.js.flow +22 -0
- package/lib/redux/actions/Preferences.js.map +1 -1
- package/lib/redux/reducers/__tests__/ModelCatalogReducer.spec.js.flow +23 -0
- package/lib/utils/datetime/DateTimeUtil.js +292 -93
- package/lib/utils/datetime/DateTimeUtil.js.flow +482 -172
- package/lib/utils/datetime/DateTimeUtil.js.map +1 -1
- package/lib/utils/datetime/__tests__/DateTime.spec.js.flow +771 -483
- package/package.json +11 -9
- package/src/constants/Constants.js +11 -0
- package/src/constants/Settings.js +6 -0
- package/src/models/attributes/DatetimeAttributeModel.js +54 -4
- package/src/models/attributes/__tests__/DatetimeAttributeModel.spec.js +9 -0
- package/src/models/attributes/__tests__/DatetimeAttributeModel_offset.spec.js +306 -0
- package/src/models/attributes/input-constraints/DatetimeFormatConstraint.js +42 -3
- package/src/react-client/client.js +2 -0
- package/src/react-server/__tests__/serverUtil.spec.js +12 -0
- package/src/react-server/serverUtil.js +2 -0
- package/src/redux/actions/Preferences.js +22 -0
- package/src/redux/reducers/__tests__/ModelCatalogReducer.spec.js +23 -0
- package/src/utils/datetime/DateTimeUtil.js +482 -172
- package/src/utils/datetime/__tests__/DateTime.spec.js +771 -483
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beinformed/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.59.0",
|
|
4
4
|
"description": "Toolbox for be informed javascript layouts",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"bugs": "http://support.beinformed.com",
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
},
|
|
84
84
|
"dependencies": {
|
|
85
85
|
"@babel/runtime-corejs3": "^7.26.0",
|
|
86
|
+
"@date-fns/tz": "^1.2.0",
|
|
86
87
|
"big.js": "^6.2.2",
|
|
87
88
|
"date-fns": "^4.1.0",
|
|
88
89
|
"deepmerge": "^4.3.1",
|
|
@@ -95,10 +96,11 @@
|
|
|
95
96
|
"klona": "^2.0.6",
|
|
96
97
|
"lodash": "^4.17.21",
|
|
97
98
|
"reselect": "^4.1.8",
|
|
98
|
-
"setimmediate": "^1.0.5"
|
|
99
|
+
"setimmediate": "^1.0.5",
|
|
100
|
+
"timezone-soft": "^1.5.2"
|
|
99
101
|
},
|
|
100
102
|
"devDependencies": {
|
|
101
|
-
"@babel/cli": "^7.
|
|
103
|
+
"@babel/cli": "^7.26.4",
|
|
102
104
|
"@babel/core": "^7.26.0",
|
|
103
105
|
"@babel/eslint-parser": "^7.25.9",
|
|
104
106
|
"@babel/eslint-plugin": "^7.25.9",
|
|
@@ -107,10 +109,10 @@
|
|
|
107
109
|
"@babel/plugin-transform-runtime": "^7.25.9",
|
|
108
110
|
"@babel/preset-env": "^7.26.0",
|
|
109
111
|
"@babel/preset-flow": "^7.25.9",
|
|
110
|
-
"@babel/preset-react": "^7.
|
|
112
|
+
"@babel/preset-react": "^7.26.3",
|
|
111
113
|
"@commitlint/cli": "^19.6.0",
|
|
112
114
|
"@commitlint/config-conventional": "^19.6.0",
|
|
113
|
-
"@testing-library/react": "^16.0
|
|
115
|
+
"@testing-library/react": "^16.1.0",
|
|
114
116
|
"auditjs": "^4.0.46",
|
|
115
117
|
"babel-jest": "^29.7.0",
|
|
116
118
|
"babel-plugin-styled-components": "^2.1.4",
|
|
@@ -124,14 +126,14 @@
|
|
|
124
126
|
"eslint-plugin-ft-flow": "^3.0.11",
|
|
125
127
|
"eslint-plugin-import": "^2.31.0",
|
|
126
128
|
"eslint-plugin-jest": "^28.9.0",
|
|
127
|
-
"eslint-plugin-jsdoc": "^50.
|
|
129
|
+
"eslint-plugin-jsdoc": "^50.6.0",
|
|
128
130
|
"eslint-plugin-react": "^7.37.2",
|
|
129
|
-
"eslint-plugin-react-hooks": "^5.
|
|
131
|
+
"eslint-plugin-react-hooks": "^5.1.0",
|
|
130
132
|
"eslint-plugin-you-dont-need-lodash-underscore": "^6.14.0",
|
|
131
133
|
"flow-bin": "^0.200.1",
|
|
132
134
|
"flow-copy-source": "^2.0.9",
|
|
133
135
|
"flow-typed": "^3.9.0",
|
|
134
|
-
"hermes-eslint": "^0.25.
|
|
136
|
+
"hermes-eslint": "^0.25.1",
|
|
135
137
|
"history": "^4.0.0",
|
|
136
138
|
"husky": "^9.1.7",
|
|
137
139
|
"jest": "^29.7.0",
|
|
@@ -141,7 +143,7 @@
|
|
|
141
143
|
"jscodeshift": "^17.1.1",
|
|
142
144
|
"lint-staged": "^15.2.10",
|
|
143
145
|
"polished": "^4.0.0",
|
|
144
|
-
"prettier": "^3.4.
|
|
146
|
+
"prettier": "^3.4.2",
|
|
145
147
|
"react": "^18.3.1",
|
|
146
148
|
"react-dom": "^18.3.1",
|
|
147
149
|
"react-helmet-async": "^2.0.5",
|
|
@@ -64,6 +64,10 @@ export const ISO_TIME_FORMAT = "HH:mm:ss";
|
|
|
64
64
|
/**
|
|
65
65
|
*/
|
|
66
66
|
export const ISO_TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS";
|
|
67
|
+
/**
|
|
68
|
+
* Offset format is appended to iso datetime and timestamp when isIncludeTimeOffsetInDateTimes = true
|
|
69
|
+
*/
|
|
70
|
+
export const DATETIME_OFFSET_FORMAT = "xxx";
|
|
67
71
|
/**
|
|
68
72
|
* week starts on monday by default
|
|
69
73
|
*/
|
|
@@ -99,6 +103,11 @@ export const IS_SYNC = typeof dataFetcher !== "undefined";
|
|
|
99
103
|
* @type {boolean}
|
|
100
104
|
*/
|
|
101
105
|
export const IS_SERVER = IS_SYNC;
|
|
106
|
+
/**
|
|
107
|
+
* @type {boolean}
|
|
108
|
+
*/
|
|
109
|
+
// $FlowExpectedError[cannot-resolve-name]
|
|
110
|
+
export const IS_GRAALJS = typeof Graal !== "undefined";
|
|
102
111
|
|
|
103
112
|
/**
|
|
104
113
|
* @type {{SUCCESS: string, ERROR: string, INFO: string, WARNING: string}}
|
|
@@ -161,6 +170,8 @@ export const ATTRIBUTE_WIDTH = {
|
|
|
161
170
|
|
|
162
171
|
export const ALL_CONTENT_IN_DATA_SETTING = "hasAllContentInData";
|
|
163
172
|
|
|
173
|
+
export const INCLUDE_TIME_OFFSET = "isIncludeTimeOffsetInDateTimes";
|
|
174
|
+
|
|
164
175
|
export const INTERNAL_LOGIN_TYPE = {
|
|
165
176
|
JAAS: "JAAS",
|
|
166
177
|
PAC4J_FORM: "PAC4J_FORM",
|
|
@@ -3,6 +3,7 @@ import { isPlainObject, has } from "../utils/helpers/objects";
|
|
|
3
3
|
import { getRepositoryResourceUrl } from "../utils/helpers/repositoryResource";
|
|
4
4
|
import {
|
|
5
5
|
ALL_CONTENT_IN_DATA_SETTING,
|
|
6
|
+
INCLUDE_TIME_OFFSET,
|
|
6
7
|
INTERNAL_LOGIN_TYPE,
|
|
7
8
|
LOGIN_TYPE,
|
|
8
9
|
LOGIN_PATH_SETTING,
|
|
@@ -200,6 +201,11 @@ export const getCaptchaPath = (
|
|
|
200
201
|
export const hasAllContentInData = (): boolean =>
|
|
201
202
|
getSetting(ALL_CONTENT_IN_DATA_SETTING, true);
|
|
202
203
|
|
|
204
|
+
/**
|
|
205
|
+
*/
|
|
206
|
+
export const isIncludeTimeOffsetInDateTimes = (): boolean =>
|
|
207
|
+
getSetting(INCLUDE_TIME_OFFSET, false);
|
|
208
|
+
|
|
203
209
|
/**
|
|
204
210
|
* Login type, only available when pac4j is configured
|
|
205
211
|
*
|
|
@@ -16,7 +16,12 @@ import DateTimeTimeFormatConstraint from "./input-constraints/DateTimeTimeFormat
|
|
|
16
16
|
import DatetimeFormatConstraint from "./input-constraints/DatetimeFormatConstraint";
|
|
17
17
|
import DateBoundaryConstraint from "./input-constraints/DateBoundaryConstraint";
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
isIncludeTimeOffsetInDateTimes,
|
|
21
|
+
ATTRIBUTE_WIDTH,
|
|
22
|
+
DATETIME_OFFSET_FORMAT,
|
|
23
|
+
ISO_DATE_FORMAT,
|
|
24
|
+
} from "../../constants";
|
|
20
25
|
|
|
21
26
|
import type {
|
|
22
27
|
FormErrorAnchor,
|
|
@@ -24,6 +29,7 @@ import type {
|
|
|
24
29
|
AttributeType,
|
|
25
30
|
ModelOptions,
|
|
26
31
|
} from "../types";
|
|
32
|
+
import type { OffsetInfo } from "../../utils/datetime/DateTimeUtil";
|
|
27
33
|
|
|
28
34
|
/**
|
|
29
35
|
*/
|
|
@@ -78,7 +84,9 @@ class DatetimeAttributeModel extends StringAttributeModel {
|
|
|
78
84
|
|
|
79
85
|
// handle old datetime values, which contained ms
|
|
80
86
|
if (this.type === "datetime" && value.includes(".")) {
|
|
81
|
-
value = value.
|
|
87
|
+
value = TimestampUtil.toFormat(value, DateTimeUtil.getIsoFormat());
|
|
88
|
+
} else {
|
|
89
|
+
value = this.formatUtil.toISO(value);
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
if (this.hasTime) {
|
|
@@ -105,6 +113,17 @@ class DatetimeAttributeModel extends StringAttributeModel {
|
|
|
105
113
|
if (this.type === "timestamp" && !this.timeInputFormat.includes("S")) {
|
|
106
114
|
timeValue = this.formatUtil.setMilliseconds(value, 0);
|
|
107
115
|
}
|
|
116
|
+
|
|
117
|
+
if (isIncludeTimeOffsetInDateTimes()) {
|
|
118
|
+
// https://github.com/date-fns/date-fns/issues/3579
|
|
119
|
+
const oldOffset = this.formatUtil.toFormat(value, DATETIME_OFFSET_FORMAT);
|
|
120
|
+
const newOffset = this.formatUtil.toFormat(
|
|
121
|
+
timeValue,
|
|
122
|
+
DATETIME_OFFSET_FORMAT,
|
|
123
|
+
);
|
|
124
|
+
timeValue = timeValue.replace(newOffset, oldOffset);
|
|
125
|
+
}
|
|
126
|
+
|
|
108
127
|
return timeValue;
|
|
109
128
|
}
|
|
110
129
|
|
|
@@ -214,7 +233,7 @@ class DatetimeAttributeModel extends StringAttributeModel {
|
|
|
214
233
|
|
|
215
234
|
/**
|
|
216
235
|
*/
|
|
217
|
-
get
|
|
236
|
+
get inputFormatWithoutOffset(): string {
|
|
218
237
|
if (this.hasDate && this.hasTime) {
|
|
219
238
|
return `${this.dateInputFormat} ${this.timeInputFormat}`.trim();
|
|
220
239
|
}
|
|
@@ -230,6 +249,19 @@ class DatetimeAttributeModel extends StringAttributeModel {
|
|
|
230
249
|
return "";
|
|
231
250
|
}
|
|
232
251
|
|
|
252
|
+
/**
|
|
253
|
+
*/
|
|
254
|
+
get inputFormat(): string {
|
|
255
|
+
if (
|
|
256
|
+
isIncludeTimeOffsetInDateTimes() &&
|
|
257
|
+
this.dateInputFormat !== "" &&
|
|
258
|
+
this.timeInputFormat !== ""
|
|
259
|
+
) {
|
|
260
|
+
return `${this.inputFormatWithoutOffset} ${DATETIME_OFFSET_FORMAT}`;
|
|
261
|
+
}
|
|
262
|
+
return this.inputFormatWithoutOffset;
|
|
263
|
+
}
|
|
264
|
+
|
|
233
265
|
/**
|
|
234
266
|
*/
|
|
235
267
|
get dateInputFormat(): string {
|
|
@@ -337,8 +369,8 @@ class DatetimeAttributeModel extends StringAttributeModel {
|
|
|
337
369
|
: "";
|
|
338
370
|
}
|
|
339
371
|
|
|
340
|
-
// format value in readonly rendering
|
|
341
372
|
/**
|
|
373
|
+
* format value in readonly rendering
|
|
342
374
|
*/
|
|
343
375
|
formatValue(value: ?string): string {
|
|
344
376
|
if (value == null || value.toString() === "") {
|
|
@@ -376,6 +408,24 @@ class DatetimeAttributeModel extends StringAttributeModel {
|
|
|
376
408
|
return "";
|
|
377
409
|
}
|
|
378
410
|
|
|
411
|
+
/**
|
|
412
|
+
*/
|
|
413
|
+
get isAmbiguous(): boolean {
|
|
414
|
+
if (typeof this.value === "string") {
|
|
415
|
+
return this.formatUtil.isAmbiguous(this.value);
|
|
416
|
+
}
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
*/
|
|
422
|
+
get offset(): OffsetInfo | null {
|
|
423
|
+
if (typeof this.value === "string") {
|
|
424
|
+
return this.formatUtil.getOffset(this.value);
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
379
429
|
/**
|
|
380
430
|
* Get minimum date
|
|
381
431
|
*/
|
|
@@ -165,4 +165,13 @@ describe("datetimeAttributeModel", () => {
|
|
|
165
165
|
|
|
166
166
|
expect(attribute.placeholder).toBe("dd-MM-yyyy");
|
|
167
167
|
});
|
|
168
|
+
|
|
169
|
+
it("does NOT contain offset in formdata property", () => {
|
|
170
|
+
const attribute = new DatetimeAttributeModel(
|
|
171
|
+
{ key: "datetime", value: "2031-12-21T17:41:21" },
|
|
172
|
+
{ type: "datetime" },
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(attribute.formdata).toEqual({ datetime: "2031-12-21T17:41:21" });
|
|
176
|
+
});
|
|
168
177
|
});
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import DatetimeAttributeModel from "../DatetimeAttributeModel";
|
|
2
|
+
import {
|
|
3
|
+
ATTRIBUTE_WIDTH,
|
|
4
|
+
INCLUDE_TIME_OFFSET,
|
|
5
|
+
setSettings,
|
|
6
|
+
} from "../../../constants";
|
|
7
|
+
import { DateTimeUtil } from "../../../utils";
|
|
8
|
+
|
|
9
|
+
describe("datetimeAttributeModel with offset on", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
setSettings({ [INCLUDE_TIME_OFFSET]: true });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should be able to create an empty DatetimeAttribute object", () => {
|
|
15
|
+
const attribute = new DatetimeAttributeModel({}, { type: "datetime" });
|
|
16
|
+
|
|
17
|
+
expect(attribute).toBeInstanceOf(DatetimeAttributeModel);
|
|
18
|
+
|
|
19
|
+
expect(attribute.type).toBe("datetime");
|
|
20
|
+
expect(attribute.format).toBe("yyyy-MM-dd'T'HH:mm:ssxxx");
|
|
21
|
+
expect(attribute.getInputValue()).toBe("");
|
|
22
|
+
expect(attribute.inputvalue).toBe("");
|
|
23
|
+
expect(attribute.readonlyvalue).toBe("");
|
|
24
|
+
|
|
25
|
+
attribute.addConstraints();
|
|
26
|
+
expect(attribute.constraintCollection).toHaveLength(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should be able to update", () => {
|
|
30
|
+
const attribute = new DatetimeAttributeModel(
|
|
31
|
+
{},
|
|
32
|
+
{
|
|
33
|
+
type: "datetime",
|
|
34
|
+
format: "dd-MM-yyyy HH:mm",
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
attribute.update("18-08-2016 13:45 +02:00");
|
|
39
|
+
expect(attribute.getInputValue()).toBe("18-08-2016 13:45 +02:00");
|
|
40
|
+
expect(attribute.getInitialInputValue("2016-08-18T13:45:23+02:00")).toBe(
|
|
41
|
+
"18-08-2016 13:45 +02:00",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(attribute.readonlyvalue).toBe("18-08-2016 13:45");
|
|
45
|
+
|
|
46
|
+
attribute.update(null);
|
|
47
|
+
expect(attribute.getInputValue()).toBe("");
|
|
48
|
+
|
|
49
|
+
attribute.update("");
|
|
50
|
+
expect(attribute.value).toBeNull();
|
|
51
|
+
|
|
52
|
+
attribute.update("aaaa");
|
|
53
|
+
expect(attribute.value).toBe("Invalid Date");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should be able to handle min and max date in only date format and renders in format", () => {
|
|
57
|
+
const attribute = new DatetimeAttributeModel(
|
|
58
|
+
{},
|
|
59
|
+
{
|
|
60
|
+
type: "datetime",
|
|
61
|
+
format: "dd-MM-yyyy HH:mm",
|
|
62
|
+
mindate: "2010-10-01T00:00:00+02:00", // CEST
|
|
63
|
+
maxdate: "2010-10-31T23:59:59+01:00", // CET
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
attribute.update("18-08-2016 13:45");
|
|
68
|
+
|
|
69
|
+
expect(attribute.isValid).toBe(false);
|
|
70
|
+
expect(attribute.formatValue(attribute.mindate)).toBe("01-10-2010 00:00");
|
|
71
|
+
expect(attribute.formatValue(attribute.maxdate)).toBe("31-10-2010 23:59");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should return AttributeWidth for dates", () => {
|
|
75
|
+
const attribute = new DatetimeAttributeModel(
|
|
76
|
+
{},
|
|
77
|
+
{
|
|
78
|
+
type: "date",
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
expect(attribute.readonlyWidth).toBe(ATTRIBUTE_WIDTH.SMALL);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should return AttributeWidth for times", () => {
|
|
85
|
+
const attribute = new DatetimeAttributeModel(
|
|
86
|
+
{},
|
|
87
|
+
{
|
|
88
|
+
type: "time",
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
expect(attribute.readonlyWidth).toBe(ATTRIBUTE_WIDTH.SMALL);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should return AttributeWidth for datetime", () => {
|
|
95
|
+
const attribute = new DatetimeAttributeModel(
|
|
96
|
+
{},
|
|
97
|
+
{
|
|
98
|
+
type: "datetime",
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
expect(attribute.readonlyWidth).toBe(ATTRIBUTE_WIDTH.MEDIUM);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("can validate datetime with datetime format", () => {
|
|
105
|
+
const datetimeFormat = new DatetimeAttributeModel(
|
|
106
|
+
{},
|
|
107
|
+
{
|
|
108
|
+
type: "datetime",
|
|
109
|
+
format: "dd-MM-yyyy HH:mm",
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(datetimeFormat.validate("19-10-2010 1:45 +02:00")).toBe(true);
|
|
114
|
+
expect(datetimeFormat.validate("19-10-2010 13:45 +02:00")).toBe(true);
|
|
115
|
+
|
|
116
|
+
expect(datetimeFormat.validate("19-10-2010")).toBe(false);
|
|
117
|
+
expect(datetimeFormat.validate("13:45")).toBe(false);
|
|
118
|
+
expect(datetimeFormat.validate("19-10-2010 1:45 am")).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("can validate datetime with only date format", () => {
|
|
122
|
+
const dateFormat = new DatetimeAttributeModel(
|
|
123
|
+
{},
|
|
124
|
+
{
|
|
125
|
+
type: "datetime",
|
|
126
|
+
format: "dd-MM-yyyy",
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(dateFormat.validate("19-10-2010")).toBe(true);
|
|
131
|
+
|
|
132
|
+
expect(dateFormat.validate("19-10-2010 1:45")).toBe(false);
|
|
133
|
+
expect(dateFormat.validate("13:45")).toBe(false);
|
|
134
|
+
expect(dateFormat.validate("19-10-2010 1:45 am")).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("can validate datetime with datetime am/pm format", () => {
|
|
138
|
+
const datetimeAmPmFormat = new DatetimeAttributeModel(
|
|
139
|
+
{},
|
|
140
|
+
{
|
|
141
|
+
type: "datetime",
|
|
142
|
+
format: "dd-MM-yyyy hh:mm a",
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(datetimeAmPmFormat.validate("19-10-2010 1:45 am +02:00")).toBe(true);
|
|
147
|
+
expect(datetimeAmPmFormat.validate("19-10-2010 1:45 pm +02:00")).toBe(true);
|
|
148
|
+
|
|
149
|
+
expect(datetimeAmPmFormat.validate("19-10-2010 1:45 +02:00")).toBe(false);
|
|
150
|
+
expect(datetimeAmPmFormat.validate("19-10-2010 13:45 +02:00")).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("can handle old datetime format with ms", () => {
|
|
154
|
+
const attribute = new DatetimeAttributeModel(
|
|
155
|
+
{ value: "2031-12-21T17:41:21.000+01:00" },
|
|
156
|
+
{
|
|
157
|
+
type: "datetime",
|
|
158
|
+
format: "dd-MM-yyyy HH:mm",
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(attribute.readonlyvalue).toBe("21-12-2031 17:41");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("returns date part of formatlabel as placeholder if attribute has date and time", () => {
|
|
166
|
+
const attribute = new DatetimeAttributeModel(
|
|
167
|
+
{},
|
|
168
|
+
{
|
|
169
|
+
type: "datetime",
|
|
170
|
+
format: "dd-MM-yyyy HH:mm",
|
|
171
|
+
formatlabel: "dd-MM-yyyy HH:mm",
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(attribute.placeholder).toBe("dd-MM-yyyy");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("contains offset in formdata property", () => {
|
|
179
|
+
const attribute = new DatetimeAttributeModel(
|
|
180
|
+
{ key: "datetime", value: "2031-12-21T17:41:21+01:00" },
|
|
181
|
+
{ type: "datetime" },
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(attribute.formdata).toEqual({
|
|
185
|
+
datetime: "2031-12-21T17:41:21+01:00",
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("handles offsets and indicates ambiguouty", () => {
|
|
190
|
+
const winter = new DatetimeAttributeModel(
|
|
191
|
+
{ key: "datetime", value: "2031-12-21T17:41:21+01:00" },
|
|
192
|
+
{ type: "datetime" },
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(winter.isAmbiguous).toBe(false);
|
|
196
|
+
expect(winter.offset).toEqual({
|
|
197
|
+
abbr: "CET",
|
|
198
|
+
label: "Central European Standard Time",
|
|
199
|
+
value: "+01:00",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const summer = new DatetimeAttributeModel(
|
|
203
|
+
{ key: "datetime", value: "2031-07-01T17:41:21+02:00" },
|
|
204
|
+
{ type: "datetime" },
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(summer.isAmbiguous).toBe(false);
|
|
208
|
+
expect(summer.offset).toEqual({
|
|
209
|
+
abbr: "CEST",
|
|
210
|
+
label: "Central European Summer Time",
|
|
211
|
+
value: "+02:00",
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const ambiguousSummer = new DatetimeAttributeModel(
|
|
215
|
+
{ key: "datetime", value: "2024-10-27T02:30:00+02:00" },
|
|
216
|
+
{ type: "datetime" },
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(ambiguousSummer.isAmbiguous).toBe(true);
|
|
220
|
+
expect(ambiguousSummer.offset).toEqual({
|
|
221
|
+
abbr: "CEST",
|
|
222
|
+
label: "Central European Summer Time",
|
|
223
|
+
value: "+02:00",
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const ambiguousWinter = new DatetimeAttributeModel(
|
|
227
|
+
{ key: "datetime", value: "2024-10-27T02:30:00+01:00" },
|
|
228
|
+
{ type: "datetime" },
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
expect(ambiguousWinter.isAmbiguous).toBe(true);
|
|
232
|
+
expect(ambiguousWinter.offset).toEqual({
|
|
233
|
+
abbr: "CET",
|
|
234
|
+
label: "Central European Standard Time",
|
|
235
|
+
value: "+01:00",
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("retrieves the correct error collection", () => {
|
|
240
|
+
const attribute = new DatetimeAttributeModel(
|
|
241
|
+
{},
|
|
242
|
+
{
|
|
243
|
+
type: "datetime",
|
|
244
|
+
format: "dd-MM-yyyy HH:mm",
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
attribute.inputvalue = "27-10-2024";
|
|
249
|
+
|
|
250
|
+
expect(attribute.errorCollection.map((error) => error.id)).toEqual([
|
|
251
|
+
"Constraint.DateTime.MissingValue",
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
attribute.inputvalue = "27-10-2024 02:30";
|
|
255
|
+
|
|
256
|
+
expect(attribute.errorCollection.map((error) => error.id)).toEqual([
|
|
257
|
+
"Constraint.DateTime.MissingOffset",
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
attribute.inputvalue = "27-10-2024 +02:00";
|
|
261
|
+
|
|
262
|
+
expect(attribute.errorCollection.map((error) => error.id)).toEqual([
|
|
263
|
+
"Constraint.DateTime.MissingValue",
|
|
264
|
+
]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("handles dst", () => {
|
|
268
|
+
const attribute1 = new DatetimeAttributeModel(
|
|
269
|
+
{ value: "2024-10-27T01:30:00+00:00" },
|
|
270
|
+
{
|
|
271
|
+
type: "datetime",
|
|
272
|
+
format: "dd-MM-yyyy HH:mm",
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(attribute1.initvalue).toBe("2024-10-27T02:30:00+01:00");
|
|
277
|
+
expect(
|
|
278
|
+
DateTimeUtil.toFormat(attribute1.value, "dd-MM-yyyy HH:mm xxx"),
|
|
279
|
+
).toBe("27-10-2024 02:30 +01:00");
|
|
280
|
+
expect(attribute1.isAmbiguous).toBe(true);
|
|
281
|
+
expect(attribute1.offset).toEqual({
|
|
282
|
+
abbr: "CET",
|
|
283
|
+
label: "Central European Standard Time",
|
|
284
|
+
value: "+01:00",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const attribute2 = new DatetimeAttributeModel(
|
|
288
|
+
{ value: "2024-10-27T00:30:00+00:00" },
|
|
289
|
+
{
|
|
290
|
+
type: "datetime",
|
|
291
|
+
format: "dd-MM-yyyy HH:mm",
|
|
292
|
+
},
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
expect(attribute2.initvalue).toBe("2024-10-27T02:30:00+02:00");
|
|
296
|
+
expect(
|
|
297
|
+
DateTimeUtil.toFormat(attribute2.value, "dd-MM-yyyy HH:mm xxx"),
|
|
298
|
+
).toBe("27-10-2024 02:30 +02:00");
|
|
299
|
+
expect(attribute2.isAmbiguous).toBe(true);
|
|
300
|
+
expect(attribute2.offset).toEqual({
|
|
301
|
+
abbr: "CEST",
|
|
302
|
+
label: "Central European Summer Time",
|
|
303
|
+
value: "+02:00",
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
DateTimeUtil,
|
|
5
5
|
} from "../../../utils/datetime/DateTimeUtil";
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
DATETIME_OFFSET_FORMAT,
|
|
9
|
+
isIncludeTimeOffsetInDateTimes,
|
|
10
|
+
} from "../../../constants";
|
|
11
|
+
|
|
7
12
|
import type { IConstraintModel } from "../../types";
|
|
8
13
|
|
|
9
14
|
/**
|
|
@@ -11,13 +16,17 @@ import type { IConstraintModel } from "../../types";
|
|
|
11
16
|
class DatetimeFormatConstraint implements IConstraintModel {
|
|
12
17
|
_type: string;
|
|
13
18
|
_format: string;
|
|
19
|
+
_formatNoOffset: string;
|
|
14
20
|
_formatLabel: string;
|
|
15
21
|
|
|
22
|
+
_id: string = "Constraint.DateTime.MissingValue";
|
|
23
|
+
|
|
16
24
|
/**
|
|
17
25
|
*/
|
|
18
26
|
constructor(type: string, format: string, formatLabel: string) {
|
|
19
27
|
this._type = type;
|
|
20
28
|
this._format = format;
|
|
29
|
+
this._formatNoOffset = format.replace(DATETIME_OFFSET_FORMAT, "").trim();
|
|
21
30
|
this._formatLabel = formatLabel;
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -30,7 +39,7 @@ class DatetimeFormatConstraint implements IConstraintModel {
|
|
|
30
39
|
/**
|
|
31
40
|
*/
|
|
32
41
|
get id(): string {
|
|
33
|
-
return
|
|
42
|
+
return this._id;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
/**
|
|
@@ -54,12 +63,24 @@ class DatetimeFormatConstraint implements IConstraintModel {
|
|
|
54
63
|
/**
|
|
55
64
|
*/
|
|
56
65
|
get defaultMessage(): string {
|
|
66
|
+
if (this._id === "Constraint.DateTime.MissingOffset") {
|
|
67
|
+
return "Please select ${daylight-label} or ${standard-label}";
|
|
68
|
+
}
|
|
57
69
|
return "Date and time should both be entered";
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
/**
|
|
61
73
|
*/
|
|
62
|
-
get parameters():
|
|
74
|
+
get parameters():
|
|
75
|
+
| { format: string }
|
|
76
|
+
| { "daylight-label": ?string, "standard-label": ?string } {
|
|
77
|
+
if (this._id === "Constraint.DateTime.MissingOffset") {
|
|
78
|
+
const offsets = DateTimeUtil.getTimezoneOffsets();
|
|
79
|
+
return {
|
|
80
|
+
"daylight-label": offsets.daylight?.label,
|
|
81
|
+
"standard-label": offsets.standard?.label,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
63
84
|
return { format: this.formatLabel };
|
|
64
85
|
}
|
|
65
86
|
|
|
@@ -80,7 +101,25 @@ class DatetimeFormatConstraint implements IConstraintModel {
|
|
|
80
101
|
return false;
|
|
81
102
|
}
|
|
82
103
|
|
|
83
|
-
|
|
104
|
+
if (this.formatUtil.hasFormat(value, this.format)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (isIncludeTimeOffsetInDateTimes()) {
|
|
109
|
+
const hasDateTime = this.formatUtil.hasFormat(
|
|
110
|
+
value,
|
|
111
|
+
this._formatNoOffset,
|
|
112
|
+
);
|
|
113
|
+
if (!hasDateTime) {
|
|
114
|
+
this._id = "Constraint.DateTime.MissingValue";
|
|
115
|
+
return false;
|
|
116
|
+
} else if (!new RegExp("[+-](0[0-9]|1[0-4]):[0-5][0-9]").test(value)) {
|
|
117
|
+
this._id = "Constraint.DateTime.MissingOffset";
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return false;
|
|
84
123
|
}
|
|
85
124
|
|
|
86
125
|
/**
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
|
|
22
22
|
import {
|
|
23
23
|
setAllContentInDataSetting,
|
|
24
|
+
setDateTimeSettings,
|
|
24
25
|
setLoginPreferences,
|
|
25
26
|
} from "../redux/actions/Preferences";
|
|
26
27
|
import { showXHRErrorNotification } from "../redux/actions/Notification";
|
|
@@ -175,6 +176,7 @@ export const setupClient = (
|
|
|
175
176
|
|
|
176
177
|
setAllContentInDataSetting(store.getState());
|
|
177
178
|
setLoginPreferences(store.getState());
|
|
179
|
+
setDateTimeSettings(store.getState());
|
|
178
180
|
|
|
179
181
|
// load existing cache from other browser tabs
|
|
180
182
|
Cache.loadOtherBrowserTabs(() => {
|
|
@@ -119,6 +119,12 @@ describe("serverUtil", () => {
|
|
|
119
119
|
"security.clients": null,
|
|
120
120
|
},
|
|
121
121
|
},
|
|
122
|
+
{
|
|
123
|
+
type: "SET_PREFERENCE",
|
|
124
|
+
payload: {
|
|
125
|
+
isIncludeTimeOffsetInDateTimes: false,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
122
128
|
]);
|
|
123
129
|
});
|
|
124
130
|
|
|
@@ -166,6 +172,12 @@ describe("serverUtil", () => {
|
|
|
166
172
|
"security.clients": null,
|
|
167
173
|
},
|
|
168
174
|
},
|
|
175
|
+
{
|
|
176
|
+
type: "SET_PREFERENCE",
|
|
177
|
+
payload: {
|
|
178
|
+
isIncludeTimeOffsetInDateTimes: false,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
169
181
|
]);
|
|
170
182
|
});
|
|
171
183
|
});
|