From 36e99c3c0f430593ec4b17140d6668b4ef4d13c8 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 21 Dec 2022 08:07:31 -0800 Subject: [PATCH] Fix calendar date display and parsing issues (#14817) Co-authored-by: Bram Kragten fixes undefined --- package.json | 1 + .../calendar/dialog-calendar-event-detail.ts | 10 +- .../calendar/dialog-calendar-event-editor.ts | 116 ++++++++---------- yarn.lock | 10 ++ 4 files changed, 69 insertions(+), 68 deletions(-) diff --git a/package.json b/package.json index 51f4fca91d..531d6e4246 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "core-js": "^3.15.2", "cropperjs": "^1.5.12", "date-fns": "^2.23.0", + "date-fns-tz": "^1.3.7", "deep-clone-simple": "^1.1.1", "deep-freeze": "^0.0.1", "fuse.js": "^6.0.0", diff --git a/src/panels/calendar/dialog-calendar-event-detail.ts b/src/panels/calendar/dialog-calendar-event-detail.ts index 276459a0da..3f16f2c82c 100644 --- a/src/panels/calendar/dialog-calendar-event-detail.ts +++ b/src/panels/calendar/dialog-calendar-event-detail.ts @@ -1,6 +1,7 @@ import "@material/mwc-button"; import { mdiCalendarClock, mdiClose } from "@mdi/js"; import { addDays, isSameDay } from "date-fns/esm"; +import { toDate } from "date-fns-tz"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { property, state } from "lit/decorators"; import { RRule, Weekday } from "rrule"; @@ -185,11 +186,12 @@ class DialogCalendarEventDetail extends LitElement { }; private _formatDateRange() { - const start = new Date(this._data!.dtstart); + // Parse a dates in the browser timezone + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const start = toDate(this._data!.dtstart, { timeZone: timeZone }); + const endValue = toDate(this._data!.dtend, { timeZone: timeZone }); // All day events should be displayed as a day earlier - const end = isDate(this._data.dtend) - ? addDays(new Date(this._data!.dtend), -1) - : new Date(this._data!.dtend); + const end = isDate(this._data.dtend) ? addDays(endValue, -1) : endValue; // The range can be shortened when the start and end are on the same day. if (isSameDay(start, end)) { if (isDate(this._data.dtstart)) { diff --git a/src/panels/calendar/dialog-calendar-event-editor.ts b/src/panels/calendar/dialog-calendar-event-editor.ts index 4efbeec587..e76341c474 100644 --- a/src/panels/calendar/dialog-calendar-event-editor.ts +++ b/src/panels/calendar/dialog-calendar-event-editor.ts @@ -7,6 +7,7 @@ import { differenceInMilliseconds, startOfHour, } from "date-fns/esm"; +import { formatInTimeZone, toDate } from "date-fns-tz"; import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -60,6 +61,12 @@ class DialogCalendarEventEditor extends LitElement { @state() private _submitting = false; + // Dates are manipulated and displayed in the browser timezone + // which may be different from the Home Assistant timezone. When + // events are persisted, they are relative to the Home Assistant + // timezone, but floating without a timezone. + private _timeZone?: string; + public showDialog(params: CalendarEventEditDialogParams): void { this._error = undefined; this._info = undefined; @@ -71,6 +78,9 @@ class DialogCalendarEventEditor extends LitElement { computeStateDomain(stateObj) === "calendar" && supportsFeature(stateObj, CalendarEntityFeature.CREATE_EVENT) )?.entity_id; + this._timeZone = + Intl.DateTimeFormat().resolvedOptions().timeZone || + this.hass.config.time_zone; if (params.entry) { const entry = params.entry!; this._allDay = isDate(entry.dtstart); @@ -281,20 +291,30 @@ class DialogCalendarEventEditor extends LitElement { private _isEditableCalendar = (entityStateObj: HassEntity) => supportsFeature(entityStateObj, CalendarEntityFeature.CREATE_EVENT); - private _getLocaleStrings = memoizeOne((startDate?: Date, endDate?: Date) => - // en-CA locale used for date format YYYY-MM-DD - // en-GB locale used for 24h time format HH:MM:SS - { - const timeZone = this.hass.config.time_zone; - return { - startDate: startDate?.toLocaleDateString("en-CA", { timeZone }), - startTime: startDate?.toLocaleTimeString("en-GB", { timeZone }), - endDate: endDate?.toLocaleDateString("en-CA", { timeZone }), - endTime: endDate?.toLocaleTimeString("en-GB", { timeZone }), - }; - } + private _getLocaleStrings = memoizeOne( + (startDate?: Date, endDate?: Date) => ({ + startDate: this._formatDate(startDate!), + startTime: this._formatTime(startDate!), + endDate: this._formatDate(endDate!), + endTime: this._formatTime(endDate!), + }) ); + // Formats a date in specified timezone, or defaulting to browser display timezone + private _formatDate(date: Date, timeZone: string = this._timeZone!): string { + return formatInTimeZone(date, timeZone, "yyyy-MM-dd"); + } + + // Formats a time in specified timezone, or defaulting to browser display timezone + private _formatTime(date: Date, timeZone: string = this._timeZone!): string { + return formatInTimeZone(date, timeZone, "HH:mm:ss"); // 24 hr + } + + // Parse a date in the browser timezone + private _parseDate(dateStr: string): Date { + return toDate(dateStr, { timeZone: this._timeZone! }); + } + private _clearInfo() { this._info = undefined; } @@ -319,27 +339,14 @@ class DialogCalendarEventEditor extends LitElement { // Store previous event duration const duration = differenceInMilliseconds(this._dtend!, this._dtstart!); - this._dtstart = new Date( - ev.detail.value + - "T" + - this._dtstart!.toLocaleTimeString("en-GB", { - timeZone: this.hass.config.time_zone, - }) + this._dtstart = this._parseDate( + `${ev.detail.value}T${this._formatTime(this._dtstart!)}` ); // Prevent that the end time can be before the start time. Try to keep the // duration the same. if (this._dtend! <= this._dtstart!) { - const newEnd = addMilliseconds(this._dtstart, duration); - // en-CA locale used for date format YYYY-MM-DD - // en-GB locale used for 24h time format HH:MM:SS - this._dtend = new Date( - `${newEnd.toLocaleDateString("en-CA", { - timeZone: this.hass.config.time_zone, - })}T${newEnd.toLocaleTimeString("en-GB", { - timeZone: this.hass.config.time_zone, - })}` - ); + this._dtend = addMilliseconds(this._dtstart, duration); this._info = this.hass.localize( "ui.components.calendar.event.end_auto_adjusted" ); @@ -347,12 +354,8 @@ class DialogCalendarEventEditor extends LitElement { } private _endDateChanged(ev: CustomEvent) { - this._dtend = new Date( - ev.detail.value + - "T" + - this._dtend!.toLocaleTimeString("en-GB", { - timeZone: this.hass.config.time_zone, - }) + this._dtend = this._parseDate( + `${ev.detail.value}T${this._formatTime(this._dtend!)}` ); } @@ -360,25 +363,14 @@ class DialogCalendarEventEditor extends LitElement { // Store previous event duration const duration = differenceInMilliseconds(this._dtend!, this._dtstart!); - this._dtstart = new Date( - this._dtstart!.toLocaleDateString("en-CA", { - timeZone: this.hass.config.time_zone, - }) + - "T" + - ev.detail.value + this._dtstart = this._parseDate( + `${this._formatDate(this._dtstart!)}T${ev.detail.value}` ); // Prevent that the end time can be before the start time. Try to keep the // duration the same. if (this._dtend! <= this._dtstart!) { - const newEnd = addMilliseconds(new Date(this._dtstart), duration); - this._dtend = new Date( - `${newEnd.toLocaleDateString("en-CA", { - timeZone: this.hass.config.time_zone, - })}T${newEnd.toLocaleTimeString("en-GB", { - timeZone: this.hass.config.time_zone, - })}` - ); + this._dtend = addMilliseconds(new Date(this._dtstart), duration); this._info = this.hass.localize( "ui.components.calendar.event.end_auto_adjusted" ); @@ -386,20 +378,12 @@ class DialogCalendarEventEditor extends LitElement { } private _endTimeChanged(ev: CustomEvent) { - this._dtend = new Date( - this._dtend!.toLocaleDateString("en-CA", { - timeZone: this.hass.config.time_zone, - }) + - "T" + - ev.detail.value + this._dtend = this._parseDate( + `${this._formatDate(this._dtend!)}T${ev.detail.value}` ); } private _calculateData() { - const { startDate, startTime, endDate, endTime } = this._getLocaleStrings( - this._dtstart, - this._dtend - ); const data: CalendarEventMutableParams = { summary: this._summary, description: this._description, @@ -408,14 +392,18 @@ class DialogCalendarEventEditor extends LitElement { dtend: "", }; if (this._allDay) { - data.dtstart = startDate!; + data.dtstart = this._formatDate(this._dtstart!); // End date/time is exclusive when persisted - data.dtend = addDays(new Date(this._dtend!), 1).toLocaleDateString( - "en-CA" - ); + data.dtend = this._formatDate(addDays(this._dtend!, 1)); } else { - data.dtstart = `${startDate}T${startTime}`; - data.dtend = `${endDate}T${endTime}`; + data.dtstart = `${this._formatDate( + this._dtstart!, + this.hass.config.time_zone + )}T${this._formatTime(this._dtstart!, this.hass.config.time_zone)}`; + data.dtend = `${this._formatDate( + this._dtend!, + this.hass.config.time_zone + )}T${this._formatTime(this._dtend!, this.hass.config.time_zone)}`; } return data; } diff --git a/yarn.lock b/yarn.lock index 40ccc226bd..c81d28f903 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6993,6 +6993,15 @@ __metadata: languageName: node linkType: hard +"date-fns-tz@npm:^1.3.7": + version: 1.3.7 + resolution: "date-fns-tz@npm:1.3.7" + peerDependencies: + date-fns: ">=2.0.0" + checksum: b749613669223056d5e6d715114c94bec57234b676d0cea0c72ca710626c81e9ea04df6441852a5fec74b42c5f27b2f076e13697ec43da360b67806a3042a10e + languageName: node + linkType: hard + "date-fns@npm:^2.23.0": version: 2.23.0 resolution: "date-fns@npm:2.23.0" @@ -9427,6 +9436,7 @@ fsevents@^1.2.7: core-js: ^3.15.2 cropperjs: ^1.5.12 date-fns: ^2.23.0 + date-fns-tz: ^1.3.7 deep-clone-simple: ^1.1.1 deep-freeze: ^0.0.1 del: ^4.0.0