mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Fix calendar date display and parsing issues (#14817)
Co-authored-by: Bram Kragten <mail@bramkragten.nl> fixes undefined
This commit is contained in:
parent
825008e24a
commit
36e99c3c0f
@ -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",
|
||||||
|
@ -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)) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user