Fix calendar date display and parsing issues (#14817)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
fixes undefined
This commit is contained in:
Allen Porter 2022-12-21 08:07:31 -08:00 committed by GitHub
parent 825008e24a
commit 36e99c3c0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 69 additions and 68 deletions

View File

@ -106,6 +106,7 @@
"core-js": "^3.15.2", "core-js": "^3.15.2",
"cropperjs": "^1.5.12", "cropperjs": "^1.5.12",
"date-fns": "^2.23.0", "date-fns": "^2.23.0",
"date-fns-tz": "^1.3.7",
"deep-clone-simple": "^1.1.1", "deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1", "deep-freeze": "^0.0.1",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",

View File

@ -1,6 +1,7 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiCalendarClock, mdiClose } from "@mdi/js"; import { mdiCalendarClock, mdiClose } from "@mdi/js";
import { addDays, isSameDay } from "date-fns/esm"; import { addDays, isSameDay } from "date-fns/esm";
import { toDate } from "date-fns-tz";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { RRule, Weekday } from "rrule"; import { RRule, Weekday } from "rrule";
@ -185,11 +186,12 @@ class DialogCalendarEventDetail extends LitElement {
}; };
private _formatDateRange() { 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 // All day events should be displayed as a day earlier
const end = isDate(this._data.dtend) const end = isDate(this._data.dtend) ? addDays(endValue, -1) : endValue;
? addDays(new Date(this._data!.dtend), -1)
: new Date(this._data!.dtend);
// The range can be shortened when the start and end are on the same day. // The range can be shortened when the start and end are on the same day.
if (isSameDay(start, end)) { if (isSameDay(start, end)) {
if (isDate(this._data.dtstart)) { if (isDate(this._data.dtstart)) {

View File

@ -7,6 +7,7 @@ import {
differenceInMilliseconds, differenceInMilliseconds,
startOfHour, startOfHour,
} from "date-fns/esm"; } from "date-fns/esm";
import { formatInTimeZone, toDate } from "date-fns-tz";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -60,6 +61,12 @@ class DialogCalendarEventEditor extends LitElement {
@state() private _submitting = false; @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 { public showDialog(params: CalendarEventEditDialogParams): void {
this._error = undefined; this._error = undefined;
this._info = undefined; this._info = undefined;
@ -71,6 +78,9 @@ class DialogCalendarEventEditor extends LitElement {
computeStateDomain(stateObj) === "calendar" && computeStateDomain(stateObj) === "calendar" &&
supportsFeature(stateObj, CalendarEntityFeature.CREATE_EVENT) supportsFeature(stateObj, CalendarEntityFeature.CREATE_EVENT)
)?.entity_id; )?.entity_id;
this._timeZone =
Intl.DateTimeFormat().resolvedOptions().timeZone ||
this.hass.config.time_zone;
if (params.entry) { if (params.entry) {
const entry = params.entry!; const entry = params.entry!;
this._allDay = isDate(entry.dtstart); this._allDay = isDate(entry.dtstart);
@ -281,20 +291,30 @@ class DialogCalendarEventEditor extends LitElement {
private _isEditableCalendar = (entityStateObj: HassEntity) => private _isEditableCalendar = (entityStateObj: HassEntity) =>
supportsFeature(entityStateObj, CalendarEntityFeature.CREATE_EVENT); supportsFeature(entityStateObj, CalendarEntityFeature.CREATE_EVENT);
private _getLocaleStrings = memoizeOne((startDate?: Date, endDate?: Date) => private _getLocaleStrings = memoizeOne(
// en-CA locale used for date format YYYY-MM-DD (startDate?: Date, endDate?: Date) => ({
// en-GB locale used for 24h time format HH:MM:SS startDate: this._formatDate(startDate!),
{ startTime: this._formatTime(startDate!),
const timeZone = this.hass.config.time_zone; endDate: this._formatDate(endDate!),
return { endTime: this._formatTime(endDate!),
startDate: startDate?.toLocaleDateString("en-CA", { timeZone }), })
startTime: startDate?.toLocaleTimeString("en-GB", { timeZone }),
endDate: endDate?.toLocaleDateString("en-CA", { timeZone }),
endTime: endDate?.toLocaleTimeString("en-GB", { timeZone }),
};
}
); );
// 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() { private _clearInfo() {
this._info = undefined; this._info = undefined;
} }
@ -319,27 +339,14 @@ class DialogCalendarEventEditor extends LitElement {
// Store previous event duration // Store previous event duration
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!); const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
this._dtstart = new Date( this._dtstart = this._parseDate(
ev.detail.value + `${ev.detail.value}T${this._formatTime(this._dtstart!)}`
"T" +
this._dtstart!.toLocaleTimeString("en-GB", {
timeZone: this.hass.config.time_zone,
})
); );
// Prevent that the end time can be before the start time. Try to keep the // Prevent that the end time can be before the start time. Try to keep the
// duration the same. // duration the same.
if (this._dtend! <= this._dtstart!) { if (this._dtend! <= this._dtstart!) {
const newEnd = addMilliseconds(this._dtstart, duration); this._dtend = 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._info = this.hass.localize( this._info = this.hass.localize(
"ui.components.calendar.event.end_auto_adjusted" "ui.components.calendar.event.end_auto_adjusted"
); );
@ -347,12 +354,8 @@ class DialogCalendarEventEditor extends LitElement {
} }
private _endDateChanged(ev: CustomEvent) { private _endDateChanged(ev: CustomEvent) {
this._dtend = new Date( this._dtend = this._parseDate(
ev.detail.value + `${ev.detail.value}T${this._formatTime(this._dtend!)}`
"T" +
this._dtend!.toLocaleTimeString("en-GB", {
timeZone: this.hass.config.time_zone,
})
); );
} }
@ -360,25 +363,14 @@ class DialogCalendarEventEditor extends LitElement {
// Store previous event duration // Store previous event duration
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!); const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
this._dtstart = new Date( this._dtstart = this._parseDate(
this._dtstart!.toLocaleDateString("en-CA", { `${this._formatDate(this._dtstart!)}T${ev.detail.value}`
timeZone: this.hass.config.time_zone,
}) +
"T" +
ev.detail.value
); );
// Prevent that the end time can be before the start time. Try to keep the // Prevent that the end time can be before the start time. Try to keep the
// duration the same. // duration the same.
if (this._dtend! <= this._dtstart!) { if (this._dtend! <= this._dtstart!) {
const newEnd = addMilliseconds(new Date(this._dtstart), duration); this._dtend = 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._info = this.hass.localize( this._info = this.hass.localize(
"ui.components.calendar.event.end_auto_adjusted" "ui.components.calendar.event.end_auto_adjusted"
); );
@ -386,20 +378,12 @@ class DialogCalendarEventEditor extends LitElement {
} }
private _endTimeChanged(ev: CustomEvent) { private _endTimeChanged(ev: CustomEvent) {
this._dtend = new Date( this._dtend = this._parseDate(
this._dtend!.toLocaleDateString("en-CA", { `${this._formatDate(this._dtend!)}T${ev.detail.value}`
timeZone: this.hass.config.time_zone,
}) +
"T" +
ev.detail.value
); );
} }
private _calculateData() { private _calculateData() {
const { startDate, startTime, endDate, endTime } = this._getLocaleStrings(
this._dtstart,
this._dtend
);
const data: CalendarEventMutableParams = { const data: CalendarEventMutableParams = {
summary: this._summary, summary: this._summary,
description: this._description, description: this._description,
@ -408,14 +392,18 @@ class DialogCalendarEventEditor extends LitElement {
dtend: "", dtend: "",
}; };
if (this._allDay) { if (this._allDay) {
data.dtstart = startDate!; data.dtstart = this._formatDate(this._dtstart!);
// End date/time is exclusive when persisted // End date/time is exclusive when persisted
data.dtend = addDays(new Date(this._dtend!), 1).toLocaleDateString( data.dtend = this._formatDate(addDays(this._dtend!, 1));
"en-CA"
);
} else { } else {
data.dtstart = `${startDate}T${startTime}`; data.dtstart = `${this._formatDate(
data.dtend = `${endDate}T${endTime}`; 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; return data;
} }

View File

@ -6993,6 +6993,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "date-fns@npm:^2.23.0":
version: 2.23.0 version: 2.23.0
resolution: "date-fns@npm:2.23.0" resolution: "date-fns@npm:2.23.0"
@ -9427,6 +9436,7 @@ fsevents@^1.2.7:
core-js: ^3.15.2 core-js: ^3.15.2
cropperjs: ^1.5.12 cropperjs: ^1.5.12
date-fns: ^2.23.0 date-fns: ^2.23.0
date-fns-tz: ^1.3.7
deep-clone-simple: ^1.1.1 deep-clone-simple: ^1.1.1
deep-freeze: ^0.0.1 deep-freeze: ^0.0.1
del: ^4.0.0 del: ^4.0.0