mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-09 10:59:50 +00:00
Migrate from date-fns-tz to @date-fns/tz (#26809)
* Add @date-fns/tz * Update calc_date * Refactor ha-date-range-picker * Refactor calendar panel * Refactor todo panel * Remove date-fns-tz * Cleanup * Move util functions * Fix comment * Reuse * Restore old check for rrulejs, update to new format
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.38.2",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.18.0",
|
||||
"@formatjs/intl-displaynames": "6.8.11",
|
||||
@@ -102,7 +103,6 @@
|
||||
"cropperjs": "1.6.2",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns-tz": "3.2.0",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
differenceInDays,
|
||||
addDays,
|
||||
} from "date-fns";
|
||||
import { toZonedTime, fromZonedTime } from "date-fns-tz";
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
@@ -22,12 +22,13 @@ const calcZonedDate = (
|
||||
fn: (date: Date, options?: any) => Date | number | boolean,
|
||||
options?
|
||||
) => {
|
||||
const inputZoned = toZonedTime(date, tz);
|
||||
const fnZoned = fn(inputZoned, options);
|
||||
if (fnZoned instanceof Date) {
|
||||
return fromZonedTime(fnZoned, tz) as Date;
|
||||
const tzDate = new TZDate(date, tz);
|
||||
const fnResult = fn(tzDate, options);
|
||||
if (fnResult instanceof Date) {
|
||||
// Convert back to regular Date in the specified timezone
|
||||
return new Date(fnResult.getTime());
|
||||
}
|
||||
return fnZoned;
|
||||
return fnResult;
|
||||
};
|
||||
|
||||
export const calcDate = (
|
||||
@@ -65,7 +66,7 @@ export const calcDateDifferenceProperty = (
|
||||
locale,
|
||||
config,
|
||||
locale.time_zone === TimeZone.server
|
||||
? toZonedTime(startDate, config.time_zone)
|
||||
? new TZDate(startDate, config.time_zone)
|
||||
: startDate
|
||||
);
|
||||
|
||||
@@ -144,3 +145,36 @@ export const shiftDateRange = (
|
||||
}
|
||||
return { start, end };
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Parses a date in browser display timezone
|
||||
* @param date - The date to parse
|
||||
* @param timezone - The timezone to parse the date in
|
||||
* @returns The parsed date as a Date object
|
||||
*/
|
||||
export const parseDate = (date: string, timezone: string): Date => {
|
||||
const tzDate = new TZDate(date, timezone);
|
||||
return new Date(tzDate.getTime());
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Formats a date in browser display timezone
|
||||
* @param date - The date to format
|
||||
* @param timezone - The timezone to format the date in
|
||||
* @returns The formatted date in YYYY-MM-DD format
|
||||
*/
|
||||
export const formatDate = (date: Date, timezone: string): string => {
|
||||
const tzDate = new TZDate(date, timezone);
|
||||
return tzDate.toISOString().split("T")[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Formats a time in browser display timezone
|
||||
* @param date - The date to format
|
||||
* @param timezone - The timezone to format the time in
|
||||
* @returns The formatted time in HH:mm:ss format
|
||||
*/
|
||||
export const formatTime = (date: Date, timezone: string): string => {
|
||||
const tzDate = new TZDate(date, timezone);
|
||||
return tzDate.toISOString().split("T")[1].split(".")[0];
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import { isThisYear } from "date-fns";
|
||||
import { fromZonedTime, toZonedTime } from "date-fns-tz";
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -275,8 +275,8 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
|
||||
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||
start = fromZonedTime(start, this.hass.config.time_zone);
|
||||
end = fromZonedTime(end, this.hass.config.time_zone);
|
||||
start = new Date(new TZDate(start, this.hass.config.time_zone).getTime());
|
||||
end = new Date(new TZDate(end, this.hass.config.time_zone).getTime());
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -290,7 +290,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
|
||||
private _formatDate(date: Date): string {
|
||||
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||
return toZonedTime(date, this.hass.config.time_zone).toISOString();
|
||||
return new TZDate(date, this.hass.config.time_zone).toISOString();
|
||||
}
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mdiCalendarClock } from "@mdi/js";
|
||||
import { toDate } from "date-fns-tz";
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import { addDays, isSameDay } from "date-fns";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -143,8 +143,8 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
this.hass.locale.time_zone,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
const start = toDate(this._data!.dtstart, { timeZone: timeZone });
|
||||
const endValue = toDate(this._data!.dtend, { timeZone: timeZone });
|
||||
const start = new TZDate(this._data!.dtstart, timeZone);
|
||||
const endValue = new TZDate(this._data!.dtend, timeZone);
|
||||
// All day events should be displayed as a day earlier
|
||||
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.
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
differenceInMilliseconds,
|
||||
startOfHour,
|
||||
} from "date-fns";
|
||||
import { formatInTimeZone, toDate } from "date-fns-tz";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -40,6 +39,11 @@ import "../lovelace/components/hui-generic-entity-row";
|
||||
import "./ha-recurrence-rule-editor";
|
||||
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
|
||||
import type { CalendarEventEditDialogParams } from "./show-dialog-calendar-event-editor";
|
||||
import {
|
||||
formatDate,
|
||||
formatTime,
|
||||
parseDate,
|
||||
} from "../../common/datetime/calc_date";
|
||||
|
||||
const CALENDAR_DOMAINS = ["calendar"];
|
||||
|
||||
@@ -303,28 +307,13 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
|
||||
private _getLocaleStrings = memoizeOne(
|
||||
(startDate?: Date, endDate?: Date) => ({
|
||||
startDate: this._formatDate(startDate!),
|
||||
startTime: this._formatTime(startDate!),
|
||||
endDate: this._formatDate(endDate!),
|
||||
endTime: this._formatTime(endDate!),
|
||||
startDate: formatDate(startDate!, this._timeZone!),
|
||||
startTime: formatTime(startDate!, this._timeZone!),
|
||||
endDate: formatDate(endDate!, this._timeZone!),
|
||||
endTime: formatTime(endDate!, this._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() {
|
||||
this._info = undefined;
|
||||
}
|
||||
@@ -349,8 +338,9 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
// Store previous event duration
|
||||
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
|
||||
|
||||
this._dtstart = this._parseDate(
|
||||
`${ev.detail.value}T${this._formatTime(this._dtstart!)}`
|
||||
this._dtstart = parseDate(
|
||||
`${ev.detail.value}T${formatTime(this._dtstart!, this._timeZone!)}`,
|
||||
this._timeZone!
|
||||
);
|
||||
|
||||
// Prevent that the end time can be before the start time. Try to keep the
|
||||
@@ -364,8 +354,9 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _endDateChanged(ev: CustomEvent) {
|
||||
this._dtend = this._parseDate(
|
||||
`${ev.detail.value}T${this._formatTime(this._dtend!)}`
|
||||
this._dtend = parseDate(
|
||||
`${ev.detail.value}T${formatTime(this._dtend!, this._timeZone!)}`,
|
||||
this._timeZone!
|
||||
);
|
||||
}
|
||||
|
||||
@@ -373,8 +364,9 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
// Store previous event duration
|
||||
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
|
||||
|
||||
this._dtstart = this._parseDate(
|
||||
`${this._formatDate(this._dtstart!)}T${ev.detail.value}`
|
||||
this._dtstart = parseDate(
|
||||
`${formatDate(this._dtstart!, this._timeZone!)}T${ev.detail.value}`,
|
||||
this._timeZone!
|
||||
);
|
||||
|
||||
// Prevent that the end time can be before the start time. Try to keep the
|
||||
@@ -388,8 +380,9 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _endTimeChanged(ev: CustomEvent) {
|
||||
this._dtend = this._parseDate(
|
||||
`${this._formatDate(this._dtend!)}T${ev.detail.value}`
|
||||
this._dtend = parseDate(
|
||||
`${formatDate(this._dtend!, this._timeZone!)}T${ev.detail.value}`,
|
||||
this._timeZone!
|
||||
);
|
||||
}
|
||||
|
||||
@@ -402,18 +395,18 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
dtend: "",
|
||||
};
|
||||
if (this._allDay) {
|
||||
data.dtstart = this._formatDate(this._dtstart!);
|
||||
data.dtstart = formatDate(this._dtstart!, this._timeZone!);
|
||||
// End date/time is exclusive when persisted
|
||||
data.dtend = this._formatDate(addDays(this._dtend!, 1));
|
||||
data.dtend = formatDate(addDays(this._dtend!, 1), this._timeZone!);
|
||||
} else {
|
||||
data.dtstart = `${this._formatDate(
|
||||
data.dtstart = `${formatDate(
|
||||
this._dtstart!,
|
||||
this.hass.config.time_zone
|
||||
)}T${this._formatTime(this._dtstart!, this.hass.config.time_zone)}`;
|
||||
data.dtend = `${this._formatDate(
|
||||
)}T${formatTime(this._dtstart!, this.hass.config.time_zone)}`;
|
||||
data.dtend = `${formatDate(
|
||||
this._dtend!,
|
||||
this.hass.config.time_zone
|
||||
)}T${this._formatTime(this._dtend!, this.hass.config.time_zone)}`;
|
||||
)}T${formatTime(this._dtend!, this.hass.config.time_zone)}`;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { formatInTimeZone, toDate } from "date-fns-tz";
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
ruleByWeekDay,
|
||||
untilValue,
|
||||
} from "./recurrence";
|
||||
import { formatDate, formatTime } from "../../common/datetime/calc_date";
|
||||
|
||||
@customElement("ha-recurrence-rule-editor")
|
||||
export class RecurrenceRuleEditor extends LitElement {
|
||||
@@ -168,7 +169,9 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
}
|
||||
if (rrule.until) {
|
||||
this._end = "on";
|
||||
this._untilDay = toDate(rrule.until, { timeZone: this.timezone });
|
||||
this._untilDay = new Date(
|
||||
new TZDate(rrule.until, this.timezone).getTime()
|
||||
);
|
||||
} else if (rrule.count) {
|
||||
this._end = "after";
|
||||
this._count = rrule.count;
|
||||
@@ -335,7 +338,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
"ui.components.calendar.event.repeat.end_on.label"
|
||||
)}
|
||||
.locale=${this.locale}
|
||||
.value=${this._formatDate(this._untilDay!)}
|
||||
.value=${formatDate(this._untilDay!, this.timezone!)}
|
||||
@value-changed=${this._onUntilChange}
|
||||
></ha-date-input>
|
||||
`
|
||||
@@ -421,9 +424,9 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
|
||||
private _onUntilChange(e: CustomEvent) {
|
||||
e.stopPropagation();
|
||||
this._untilDay = toDate(e.detail.value + "T00:00:00", {
|
||||
timeZone: this.timezone,
|
||||
});
|
||||
this._untilDay = new Date(
|
||||
new TZDate(e.detail.value + "T00:00:00", this.timezone).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
// Reset the weekday selected when there is only a single value
|
||||
@@ -458,20 +461,22 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
let contentline = RRule.optionsToString(options);
|
||||
if (this._untilDay) {
|
||||
// The UNTIL value should be inclusive of the last event instance
|
||||
const until = toDate(
|
||||
this._formatDate(this._untilDay!) +
|
||||
const until = new TZDate(
|
||||
formatDate(this._untilDay!, this.timezone!) +
|
||||
"T" +
|
||||
this._formatTime(this.dtstart!),
|
||||
{ timeZone: this.timezone }
|
||||
formatTime(this.dtstart!, this.timezone!),
|
||||
this.timezone
|
||||
);
|
||||
// rrule.js can't compute some UNTIL variations so we compute that ourself. Must be
|
||||
// in the same format as dtstart.
|
||||
const format = this.allDay ? "yyyyMMdd" : "yyyyMMdd'T'HHmmss";
|
||||
const newUntilValue = formatInTimeZone(
|
||||
until,
|
||||
this.hass.config.time_zone,
|
||||
format
|
||||
);
|
||||
let newUntilValue;
|
||||
if (this.allDay) {
|
||||
// For all-day events, only use the date part
|
||||
newUntilValue = until.toISOString().split("T")[0].replace(/-/g, "");
|
||||
} else {
|
||||
// For timed events, include the time part
|
||||
newUntilValue = until.toISOString().replace(/[-:]/g, "").split(".")[0];
|
||||
}
|
||||
contentline += `;UNTIL=${newUntilValue}`;
|
||||
}
|
||||
return contentline.slice(6); // Strip "RRULE:" prefix
|
||||
@@ -492,16 +497,6 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
// Formats a date in browser display timezone
|
||||
private _formatDate(date: Date): string {
|
||||
return formatInTimeZone(date, this.timezone!, "yyyy-MM-dd");
|
||||
}
|
||||
|
||||
// Formats a time in browser display timezone
|
||||
private _formatTime(date: Date): string {
|
||||
return formatInTimeZone(date, this.timezone!, "HH:mm:ss");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-textfield,
|
||||
ha-select {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatInTimeZone, toDate } from "date-fns-tz";
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -239,7 +239,8 @@ class DialogTodoItemEditor extends LitElement {
|
||||
|
||||
// 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");
|
||||
const tzDate = new TZDate(date, timeZone);
|
||||
return tzDate.toISOString().split("T")[0]; // Get YYYY-MM-DD format
|
||||
}
|
||||
|
||||
// Formats a time in specified timezone, or defaulting to browser display timezone
|
||||
@@ -247,14 +248,15 @@ class DialogTodoItemEditor extends LitElement {
|
||||
date: Date,
|
||||
timeZone: string = this._timeZone!
|
||||
): string | undefined {
|
||||
return this._hasTime
|
||||
? formatInTimeZone(date, timeZone, "HH:mm:ss")
|
||||
: undefined; // 24 hr
|
||||
if (!this._hasTime) return undefined;
|
||||
const tzDate = new TZDate(date, timeZone);
|
||||
return tzDate.toISOString().split("T")[1].split(".")[0]; // Get HH:mm:ss format
|
||||
}
|
||||
|
||||
// Parse a date in the browser timezone
|
||||
private _parseDate(dateStr: string): Date {
|
||||
return toDate(dateStr, { timeZone: this._timeZone! });
|
||||
const tzDate = new TZDate(dateStr, this._timeZone!);
|
||||
return new Date(tzDate.getTime());
|
||||
}
|
||||
|
||||
private _checkedCanged(ev) {
|
||||
|
||||
18
yarn.lock
18
yarn.lock
@@ -1330,6 +1330,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@date-fns/tz@npm:1.4.1":
|
||||
version: 1.4.1
|
||||
resolution: "@date-fns/tz@npm:1.4.1"
|
||||
checksum: 10/062097590005cce3da4c7d9880f9c77d386cff5b4dd58fa3dde3c346a8b2e4f4a8025a613306351a7cad8eb71178a0f67b4840d5884f73aa4c759085fac92063
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@egjs/hammerjs@npm:2.0.17":
|
||||
version: 2.0.17
|
||||
resolution: "@egjs/hammerjs@npm:2.0.17"
|
||||
@@ -7082,15 +7089,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"date-fns-tz@npm:3.2.0":
|
||||
version: 3.2.0
|
||||
resolution: "date-fns-tz@npm:3.2.0"
|
||||
peerDependencies:
|
||||
date-fns: ^3.0.0 || ^4.0.0
|
||||
checksum: 10/8ab4745f00b40381220f0a7a2ec16e217cb629d4018a19047264d289dd260322baa23e19b3ed63c7e553f9ad34bea9dea105391132930a3e141e9a0a53e54af2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"date-fns@npm:4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "date-fns@npm:4.1.0"
|
||||
@@ -9292,6 +9290,7 @@ __metadata:
|
||||
"@codemirror/search": "npm:6.5.11"
|
||||
"@codemirror/state": "npm:6.5.2"
|
||||
"@codemirror/view": "npm:6.38.2"
|
||||
"@date-fns/tz": "npm:1.4.1"
|
||||
"@egjs/hammerjs": "npm:2.0.17"
|
||||
"@formatjs/intl-datetimeformat": "npm:6.18.0"
|
||||
"@formatjs/intl-displaynames": "npm:6.8.11"
|
||||
@@ -9388,7 +9387,6 @@ __metadata:
|
||||
cropperjs: "npm:1.6.2"
|
||||
culori: "npm:4.0.2"
|
||||
date-fns: "npm:4.1.0"
|
||||
date-fns-tz: "npm:3.2.0"
|
||||
deep-clone-simple: "npm:1.1.1"
|
||||
deep-freeze: "npm:0.0.1"
|
||||
del: "npm:8.0.0"
|
||||
|
||||
Reference in New Issue
Block a user