Skip to content

Commit

Permalink
UIEXT-941: Work around date-fns-tz issues
Browse files Browse the repository at this point in the history
See the open issue here: marnusw/date-fns-tz#302

UIEXT-941 (Date&Time widget displays always browser timezone)
  • Loading branch information
pbaern committed Nov 20, 2024
1 parent d198d03 commit 2a2b44e
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 59 deletions.
61 changes: 56 additions & 5 deletions org.knime.js.pagebuilder/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion org.knime.js.pagebuilder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"license": "UNLICENSED",
"author": "KNIME AG, Zurich, Switzerland",
"dependencies": {
"@knime/components": "1.8.0",
"@knime/components": "1.9.0",
"@knime/styles": "1.1.1",
"@knime/ui-extension-renderer": "1.1.40",
"@knime/ui-extension-service": "1.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { DateTimeInput } from "@knime/components/date-time-input";
import ErrorMessage from "../baseElements/text/ErrorMessage.vue";
import { getLocalTimeZone, updateTime } from "@knime/utils";
import { format, zonedTimeToUtc, utcToZonedTime } from "date-fns-tz";
import { format } from "date-fns-tz";
import { fromZonedTime, toZonedTime } from "@/util/widgetUtil/dateTime";
/**
* DateTimeWidget.
Expand Down Expand Up @@ -109,7 +110,7 @@ export default {
return this.parseKnimeDateString(this.value.datestring);
},
dateObject() {
return zonedTimeToUtc(this.dateValue.datestring, this.timezone);
return fromZonedTime(this.dateValue.datestring, this.timezone);
},
timezone() {
return this.dateValue.zonestring;
Expand All @@ -126,9 +127,9 @@ export default {
this.viewRep.min,
);
if (this.viewRep.useminexectime) {
return zonedTimeToUtc(this.execTime, this.localTimeZone);
return this.execTime;
}
return zonedTimeToUtc(datestring, zonestring);
return fromZonedTime(datestring, zonestring);
}
return null;
},
Expand All @@ -138,9 +139,9 @@ export default {
this.viewRep.max,
);
if (this.viewRep.usemaxexectime) {
return zonedTimeToUtc(this.execTime, this.localTimeZone);
return this.execTime;
}
return zonedTimeToUtc(datestring, zonestring);
return fromZonedTime(datestring, zonestring);
}
return null;
},
Expand All @@ -161,7 +162,9 @@ export default {
* @returns {{zonestring: String, datestring: String}}
*/
parseKnimeDateString(dateAndZoneString) {
let match = dateAndZoneString.match(/(.+)\[(.+)]/) || [null, "", ""];
let match = dateAndZoneString.match(
/(.+?)(?:Z|[+-]\d\d:?(?:\d\d)?)\[(.+)]/,
) || [null, "", ""];
return {
datestring: match[1],
zonestring: match[2],
Expand All @@ -171,8 +174,11 @@ export default {
return format(date, "yyyy-MM-dd'T'HH:mm:ss.SSS");
},
onChange(date, timezone) {
let zonedDate = utcToZonedTime(date, timezone);
let value = this.formatDate(zonedDate);
let zonedDate = toZonedTime(date, timezone);
// this.formatDate takes the local timezone into account, so we do not want to use it here
let value = zonedDate.toISOString().replace("Z", "");
this.dateValue.datestring = value;
this.dateValue.zonestring = timezone;
this.publishUpdate(value, timezone);
},
publishUpdate(datestring, zonestring) {
Expand All @@ -190,7 +196,17 @@ export default {
this.onChange(date, this.timezone);
},
onTimezoneChange(timezone) {
this.onChange(this.dateObject, timezone);
const existingTimeAsZonedTime = toZonedTime(
this.dateObject,
this.timezone,
);
const shiftedTime = fromZonedTime(existingTimeAsZonedTime, timezone);
/**
* Calling
* this.onChange(this.dateObject, timezone);
* would instead update the date object with the new timezone, which is not what we want.
*/
this.onChange(shiftedTime, timezone);
},
nowButtonClicked() {
let now = new Date(Date.now());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ describe("DateTimeWidget.vue", () => {
});

describe("events and actions", () => {
it("emits @updateWidget if timezone changes", () => {
it("emits @updateWidget if timezone changes while keeping the local time", () => {
let wrapper = mount(DateTimeWidget, {
props: propsAll,
stubs: {
Expand All @@ -495,8 +495,11 @@ describe("DateTimeWidget.vue", () => {
expect(
wrapper.emitted("updateWidget")[1][0].update[
"viewRepresentation.currentValue"
].zonestring,
).toBe("Asia/Bangkok");
],
).toStrictEqual({
zonestring: "Asia/Bangkok",
datestring: "2020-05-03T09:54:55.000",
});
});

it("now button sets date, time and timezone to current values and location", () => {
Expand Down Expand Up @@ -556,47 +559,71 @@ describe("DateTimeWidget.vue", () => {
).toBe(0);
});

it("emits @updateWidget if DateTimeInput emits @input", () => {
let wrapper = mount(DateTimeWidget, {
props: propsAll,
stubs: {
"client-only": "<div><slot /></div>",
},
...context,
});

const testValue = "2020-10-14T13:32:45.153";
const input = wrapper.findComponent(DateTimeInput);
input.vm.$emit("update:modelValue", new Date(testValue));

expect(wrapper.emitted("updateWidget")).toBeTruthy();
expect(wrapper.emitted("updateWidget")[1][0]).toStrictEqual({
nodeId: propsAll.nodeId,
update: {
"viewRepresentation.currentValue": {
datestring: testValue,
zonestring: "Europe/Rome",
it.each([
{
timezone: "UTC",
offset: 0,
},
{
timezone: "Europe/Rome",
offset: 2,
},
])(
"emits @updateWidget if DateTimeInput emits @input",
({ timezone, offset }) => {
let wrapper = mount(DateTimeWidget, {
props: {
...propsAll,
valuePair: {
datestring: "2020-01-01T00:00:00.000",
zonestring: timezone,
},
},
},
});
});
stubs: {
"client-only": "<div><slot /></div>",
},
...context,
});

const input = wrapper.findComponent(DateTimeInput);
const testHours = 13;
input.vm.$emit(
"update:modelValue",
Date.UTC(2020, 9, 14, testHours, 32, 45, 153),
);

expect(wrapper.emitted("updateWidget")).toBeTruthy();
expect(wrapper.emitted("updateWidget")[0][0]).toStrictEqual({
nodeId: propsAll.nodeId,
update: {
"viewRepresentation.currentValue": {
datestring: `2020-10-14T${testHours + offset}:32:45.153`,
zonestring: timezone,
},
},
});
},
);
});

describe("methods", () => {
it("parses knime date and timezone strings", () => {
let wrapper = mount(DateTimeWidget, {
props: propsAll,
stubs: {
"client-only": "<div><slot /></div>",
},
...context,
});
const res = wrapper.vm.parseKnimeDateString(
"2020-10-10T13:32:45.153[Europe/Berlin]",
);
expect(res.datestring).toBe("2020-10-10T13:32:45.153");
expect(res.zonestring).toBe("Europe/Berlin");
});
it.each(["+02:00", "+02", "+0200", "Z"])(
"parses knime date and timezone strings",
(offset) => {
let wrapper = mount(DateTimeWidget, {
props: propsAll,
stubs: {
"client-only": "<div><slot /></div>",
},
...context,
});
const res = wrapper.vm.parseKnimeDateString(
`2020-10-10T13:32:45.153${offset}[Europe/Rome]`,
);
expect(res.datestring).toBe("2020-10-10T13:32:45.153");
expect(res.zonestring).toBe("Europe/Rome");
},
);

it("parses broken knime date and timezone strings", () => {
let wrapper = mount(DateTimeWidget, {
Expand All @@ -606,7 +633,9 @@ describe("DateTimeWidget.vue", () => {
},
...context,
});
const res = wrapper.vm.parseKnimeDateString("2020-10-10T13:32:45.153[");
const res = wrapper.vm.parseKnimeDateString(
"2020-10-10T13:32:45.153[UTC]",
);
expect(res.datestring).toBe("");
expect(res.zonestring).toBe("");
});
Expand Down Expand Up @@ -645,7 +674,7 @@ describe("DateTimeWidget.vue", () => {
it("invalidates if min bound is not kept", () => {
propsAll.nodeConfig.viewRepresentation.usemin = true;
propsAll.nodeConfig.viewRepresentation.min =
"2020-10-10T13:32:45.153[Europe/Berlin]";
"2020-10-10T13:32:45.153+02:00[Europe/Berlin]";
propsAll.nodeConfig.viewRepresentation.usemax = false;
let wrapper = mount(DateTimeWidget, {
props: propsAll,
Expand All @@ -665,7 +694,7 @@ describe("DateTimeWidget.vue", () => {
it("invalidates if max bound is not kept", () => {
propsAll.nodeConfig.viewRepresentation.usemax = true;
propsAll.nodeConfig.viewRepresentation.max =
"2020-04-10T13:32:45.153[Europe/Berlin]";
"2020-04-10T13:32:45.153+02:00[Europe/Berlin]";
propsAll.nodeConfig.viewRepresentation.usemin = false;
let wrapper = mount(DateTimeWidget, {
props: propsAll,
Expand Down
Loading

0 comments on commit 2a2b44e

Please sign in to comment.