@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/esm/constants/Constants.js +10 -0
  3. package/esm/constants/Constants.js.map +1 -1
  4. package/esm/constants/Settings.js +5 -1
  5. package/esm/constants/Settings.js.map +1 -1
  6. package/esm/models/attributes/DatetimeAttributeModel.js +39 -4
  7. package/esm/models/attributes/DatetimeAttributeModel.js.map +1 -1
  8. package/esm/models/attributes/input-constraints/DatetimeFormatConstraint.js +31 -2
  9. package/esm/models/attributes/input-constraints/DatetimeFormatConstraint.js.map +1 -1
  10. package/esm/react-client/client.js +2 -1
  11. package/esm/react-client/client.js.map +1 -1
  12. package/esm/react-server/serverUtil.js +2 -1
  13. package/esm/react-server/serverUtil.js.map +1 -1
  14. package/esm/redux/actions/Preferences.js +15 -1
  15. package/esm/redux/actions/Preferences.js.map +1 -1
  16. package/esm/utils/datetime/DateTimeUtil.js +292 -94
  17. package/esm/utils/datetime/DateTimeUtil.js.map +1 -1
  18. package/lib/constants/Constants.js +11 -1
  19. package/lib/constants/Constants.js.flow +11 -0
  20. package/lib/constants/Constants.js.map +1 -1
  21. package/lib/constants/Settings.js +7 -2
  22. package/lib/constants/Settings.js.flow +6 -0
  23. package/lib/constants/Settings.js.map +1 -1
  24. package/lib/models/attributes/DatetimeAttributeModel.js +38 -3
  25. package/lib/models/attributes/DatetimeAttributeModel.js.flow +54 -4
  26. package/lib/models/attributes/DatetimeAttributeModel.js.map +1 -1
  27. package/lib/models/attributes/__tests__/DatetimeAttributeModel.spec.js.flow +9 -0
  28. package/lib/models/attributes/__tests__/DatetimeAttributeModel_offset.spec.js.flow +306 -0
  29. package/lib/models/attributes/input-constraints/DatetimeFormatConstraint.js +31 -2
  30. package/lib/models/attributes/input-constraints/DatetimeFormatConstraint.js.flow +42 -3
  31. package/lib/models/attributes/input-constraints/DatetimeFormatConstraint.js.map +1 -1
  32. package/lib/react-client/client.js +1 -0
  33. package/lib/react-client/client.js.flow +2 -0
  34. package/lib/react-client/client.js.map +1 -1
  35. package/lib/react-server/__tests__/serverUtil.spec.js.flow +12 -0
  36. package/lib/react-server/serverUtil.js +1 -0
  37. package/lib/react-server/serverUtil.js.flow +2 -0
  38. package/lib/react-server/serverUtil.js.map +1 -1
  39. package/lib/redux/actions/Preferences.js +17 -2
  40. package/lib/redux/actions/Preferences.js.flow +22 -0
  41. package/lib/redux/actions/Preferences.js.map +1 -1
  42. package/lib/redux/reducers/__tests__/ModelCatalogReducer.spec.js.flow +23 -0
  43. package/lib/utils/datetime/DateTimeUtil.js +292 -93
  44. package/lib/utils/datetime/DateTimeUtil.js.flow +482 -172
  45. package/lib/utils/datetime/DateTimeUtil.js.map +1 -1
  46. package/lib/utils/datetime/__tests__/DateTime.spec.js.flow +771 -483
  47. package/package.json +11 -9
  48. package/src/constants/Constants.js +11 -0
  49. package/src/constants/Settings.js +6 -0
  50. package/src/models/attributes/DatetimeAttributeModel.js +54 -4
  51. package/src/models/attributes/__tests__/DatetimeAttributeModel.spec.js +9 -0
  52. package/src/models/attributes/__tests__/DatetimeAttributeModel_offset.spec.js +306 -0
  53. package/src/models/attributes/input-constraints/DatetimeFormatConstraint.js +42 -3
  54. package/src/react-client/client.js +2 -0
  55. package/src/react-server/__tests__/serverUtil.spec.js +12 -0
  56. package/src/react-server/serverUtil.js +2 -0
  57. package/src/redux/actions/Preferences.js +22 -0
  58. package/src/redux/reducers/__tests__/ModelCatalogReducer.spec.js +23 -0
  59. package/src/utils/datetime/DateTimeUtil.js +482 -172
  60. 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.58.4",
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.25.9",
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.25.9",
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.1",
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.5.0",
129
+ "eslint-plugin-jsdoc": "^50.6.0",
128
130
  "eslint-plugin-react": "^7.37.2",
129
- "eslint-plugin-react-hooks": "^5.0.0",
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.0",
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.0",
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 { ATTRIBUTE_WIDTH, ISO_DATE_FORMAT } from "../../constants";
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.split(".")[0];
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 inputFormat(): string {
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 "Constraint.DateTime.MissingValue";
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(): { format: string } {
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
- return this.formatUtil.hasFormat(value, this.format);
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
  });