mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-20 07:46:37 +00:00
Add monthly variations for recurrence rules (#14849)
* Add variations on monthly recurrence rules * Recurrence rule code simplificiation * Invalidate when the interval changes * update * Update ha-recurrence-rule-editor.ts Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
b99a139f51
commit
6a15216104
@ -4,15 +4,11 @@ import { addDays, isSameDay } from "date-fns/esm";
|
|||||||
import { toDate } from "date-fns-tz";
|
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 { formatDate } from "../../common/datetime/format_date";
|
import { formatDate } from "../../common/datetime/format_date";
|
||||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||||
import { formatTime } from "../../common/datetime/format_time";
|
import { formatTime } from "../../common/datetime/format_time";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { capitalizeFirstLetter } from "../../common/string/capitalize-first-letter";
|
|
||||||
import { isDate } from "../../common/string/is_date";
|
import { isDate } from "../../common/string/is_date";
|
||||||
import { dayNames } from "../../common/translations/day_names";
|
|
||||||
import { monthNames } from "../../common/translations/month_names";
|
|
||||||
import "../../components/entity/state-info";
|
import "../../components/entity/state-info";
|
||||||
import "../../components/ha-date-input";
|
import "../../components/ha-date-input";
|
||||||
import "../../components/ha-time-input";
|
import "../../components/ha-time-input";
|
||||||
@ -23,10 +19,10 @@ import {
|
|||||||
import { haStyleDialog } from "../../resources/styles";
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../lovelace/components/hui-generic-entity-row";
|
import "../lovelace/components/hui-generic-entity-row";
|
||||||
import "./ha-recurrence-rule-editor";
|
|
||||||
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
|
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
|
||||||
import { CalendarEventDetailDialogParams } from "./show-dialog-calendar-event-detail";
|
import { CalendarEventDetailDialogParams } from "./show-dialog-calendar-event-detail";
|
||||||
import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor";
|
import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor";
|
||||||
|
import { renderRRuleAsText } from "./recurrence";
|
||||||
|
|
||||||
class DialogCalendarEventDetail extends LitElement {
|
class DialogCalendarEventDetail extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -137,54 +133,16 @@ class DialogCalendarEventDetail extends LitElement {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const rule = RRule.fromString(`RRULE:${value}`);
|
const ruleText = renderRRuleAsText(this.hass, value);
|
||||||
if (rule.isFullyConvertibleToText()) {
|
if (ruleText !== undefined) {
|
||||||
return html`<div id="text">
|
return html`<div id="text">${ruleText}</div>`;
|
||||||
${capitalizeFirstLetter(
|
|
||||||
rule.toText(
|
|
||||||
this._translateRRuleElement,
|
|
||||||
{
|
|
||||||
dayNames: dayNames(this.hass.locale),
|
|
||||||
monthNames: monthNames(this.hass.locale),
|
|
||||||
tokens: {},
|
|
||||||
},
|
|
||||||
this._formatDate
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`<div id="text">Cannot convert recurrence rule</div>`;
|
return html`<div id="text">Cannot convert recurrence rule</div>`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return "Error while processing the rule";
|
return "Error while processing the rule";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _translateRRuleElement = (id: string | number | Weekday): string => {
|
|
||||||
if (typeof id === "string") {
|
|
||||||
return this.hass.localize(`ui.components.calendar.event.rrule.${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
private _formatDate = (year: number, month: string, day: number): string => {
|
|
||||||
if (!year || !month || !day) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build date so we can then format it
|
|
||||||
const date = new Date();
|
|
||||||
date.setFullYear(year);
|
|
||||||
// As input we already get the localized month name, so we now unfortunately
|
|
||||||
// need to convert it back to something Date can work with. The already localized
|
|
||||||
// months names are a must in the RRule.Language structure (an empty string[] would
|
|
||||||
// mean we get undefined months input in this method here).
|
|
||||||
date.setMonth(monthNames(this.hass.locale).indexOf(month));
|
|
||||||
date.setDate(day);
|
|
||||||
return formatDate(date, this.hass.locale);
|
|
||||||
};
|
|
||||||
|
|
||||||
private _formatDateRange() {
|
private _formatDateRange() {
|
||||||
// Parse a dates in the browser timezone
|
// Parse a dates in the browser timezone
|
||||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
@ -248,6 +248,8 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ha-recurrence-rule-editor
|
<ha-recurrence-rule-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.dtstart=${this._dtstart}
|
||||||
.locale=${this.hass.locale}
|
.locale=${this.hass.locale}
|
||||||
.timezone=${this.hass.config.time_zone}
|
.timezone=${this.hass.config.time_zone}
|
||||||
.value=${this._rrule || ""}
|
.value=${this._rrule || ""}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { SelectedDetail } from "@material/mwc-list";
|
import type { SelectedDetail } from "@material/mwc-list";
|
||||||
import { css, html, LitElement, PropertyValues } from "lit";
|
import { css, html, LitElement, PropertyValues } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import type { Options, WeekdayStr } from "rrule";
|
import type { Options, WeekdayStr } from "rrule";
|
||||||
import { ByWeekday, RRule, Weekday } from "rrule";
|
import { ByWeekday, RRule, Weekday } from "rrule";
|
||||||
@ -16,22 +16,31 @@ import {
|
|||||||
convertFrequency,
|
convertFrequency,
|
||||||
convertRepeatFrequency,
|
convertRepeatFrequency,
|
||||||
DEFAULT_COUNT,
|
DEFAULT_COUNT,
|
||||||
|
getWeekday,
|
||||||
getWeekdays,
|
getWeekdays,
|
||||||
|
getMonthlyRepeatItems,
|
||||||
intervalSuffix,
|
intervalSuffix,
|
||||||
RepeatEnd,
|
RepeatEnd,
|
||||||
RepeatFrequency,
|
RepeatFrequency,
|
||||||
ruleByWeekDay,
|
ruleByWeekDay,
|
||||||
untilValue,
|
untilValue,
|
||||||
WEEKDAY_NAME,
|
WEEKDAY_NAME,
|
||||||
|
MonthlyRepeatItem,
|
||||||
|
getMonthlyRepeatWeekdayFromRule,
|
||||||
|
getMonthdayRepeatFromRule,
|
||||||
} from "./recurrence";
|
} from "./recurrence";
|
||||||
import "../../components/ha-date-input";
|
import "../../components/ha-date-input";
|
||||||
|
|
||||||
@customElement("ha-recurrence-rule-editor")
|
@customElement("ha-recurrence-rule-editor")
|
||||||
export class RecurrenceRuleEditor extends LitElement {
|
export class RecurrenceRuleEditor extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public disabled = false;
|
@property() public disabled = false;
|
||||||
|
|
||||||
@property() public value = "";
|
@property() public value = "";
|
||||||
|
|
||||||
|
@property() public dtstart?: Date;
|
||||||
|
|
||||||
@property({ attribute: false }) public locale!: HomeAssistant["locale"];
|
@property({ attribute: false }) public locale!: HomeAssistant["locale"];
|
||||||
|
|
||||||
@property() public timezone?: string;
|
@property() public timezone?: string;
|
||||||
@ -44,14 +53,24 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
|
|
||||||
@state() private _weekday: Set<WeekdayStr> = new Set<WeekdayStr>();
|
@state() private _weekday: Set<WeekdayStr> = new Set<WeekdayStr>();
|
||||||
|
|
||||||
|
@state() private _monthlyRepeat?: string;
|
||||||
|
|
||||||
|
@state() private _monthlyRepeatWeekday?: Weekday;
|
||||||
|
|
||||||
|
@state() private _monthday?: number;
|
||||||
|
|
||||||
@state() private _end: RepeatEnd = "never";
|
@state() private _end: RepeatEnd = "never";
|
||||||
|
|
||||||
@state() private _count?: number;
|
@state() private _count?: number;
|
||||||
|
|
||||||
@state() private _until?: Date;
|
@state() private _until?: Date;
|
||||||
|
|
||||||
|
@query("#monthly") private _monthlyRepeatSelect!: HaSelect;
|
||||||
|
|
||||||
private _allWeekdays?: WeekdayStr[];
|
private _allWeekdays?: WeekdayStr[];
|
||||||
|
|
||||||
|
private _monthlyRepeatItems: MonthlyRepeatItem[] = [];
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues) {
|
protected willUpdate(changedProps: PropertyValues) {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
@ -61,12 +80,45 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!changedProps.has("value") || this._computedRRule === this.value) {
|
if (changedProps.has("dtstart") || changedProps.has("_interval")) {
|
||||||
|
this._monthlyRepeatItems = this.dtstart
|
||||||
|
? getMonthlyRepeatItems(this.hass, this._interval, this.dtstart)
|
||||||
|
: [];
|
||||||
|
this._computeWeekday();
|
||||||
|
const selectElement = this._monthlyRepeatSelect;
|
||||||
|
if (selectElement) {
|
||||||
|
const oldSelected = selectElement.index;
|
||||||
|
selectElement.select(-1);
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
selectElement.select(changedProps.has("dtstart") ? 0 : oldSelected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
changedProps.has("timezone") ||
|
||||||
|
changedProps.has("_freq") ||
|
||||||
|
changedProps.has("_interval") ||
|
||||||
|
changedProps.has("_weekday") ||
|
||||||
|
changedProps.has("_monthlyRepeatWeekday") ||
|
||||||
|
changedProps.has("_monthday") ||
|
||||||
|
changedProps.has("_end") ||
|
||||||
|
changedProps.has("_count") ||
|
||||||
|
changedProps.has("_until")
|
||||||
|
) {
|
||||||
|
this._updateRule();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._computedRRule === this.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._interval = 1;
|
this._interval = 1;
|
||||||
this._weekday.clear();
|
this._weekday.clear();
|
||||||
|
this._monthlyRepeat = undefined;
|
||||||
|
this._monthday = undefined;
|
||||||
|
this._monthlyRepeatWeekday = undefined;
|
||||||
this._end = "never";
|
this._end = "never";
|
||||||
this._count = undefined;
|
this._count = undefined;
|
||||||
this._until = undefined;
|
this._until = undefined;
|
||||||
@ -88,6 +140,14 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
if (rrule.interval) {
|
if (rrule.interval) {
|
||||||
this._interval = rrule.interval;
|
this._interval = rrule.interval;
|
||||||
}
|
}
|
||||||
|
this._monthlyRepeatWeekday = getMonthlyRepeatWeekdayFromRule(rrule);
|
||||||
|
if (this._monthlyRepeatWeekday) {
|
||||||
|
this._monthlyRepeat = `BYDAY=${this._monthlyRepeatWeekday.toString()}`;
|
||||||
|
}
|
||||||
|
this._monthday = getMonthdayRepeatFromRule(rrule);
|
||||||
|
if (this._monthday) {
|
||||||
|
this._monthlyRepeat = `BYMONTHDAY=${this._monthday}`;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
this._freq === "weekly" &&
|
this._freq === "weekly" &&
|
||||||
rrule.byweekday &&
|
rrule.byweekday &&
|
||||||
@ -129,7 +189,28 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderMonthly() {
|
renderMonthly() {
|
||||||
return this.renderInterval();
|
return html`
|
||||||
|
${this.renderInterval()}
|
||||||
|
${this._monthlyRepeatItems.length > 0
|
||||||
|
? html`<ha-select
|
||||||
|
id="monthly"
|
||||||
|
label="Repeat Monthly"
|
||||||
|
@selected=${this._onMonthlyDetailSelected}
|
||||||
|
.value=${this._monthlyRepeat || this._monthlyRepeatItems[0]?.value}
|
||||||
|
@closed=${stopPropagation}
|
||||||
|
fixedMenuPosition
|
||||||
|
naturalMenuWidth
|
||||||
|
>
|
||||||
|
${this._monthlyRepeatItems!.map(
|
||||||
|
(item) => html`
|
||||||
|
<ha-list-item .value=${item.value} .item=${item}>
|
||||||
|
${item.label}
|
||||||
|
</ha-list-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-select>`
|
||||||
|
: html``}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderWeekly() {
|
renderWeekly() {
|
||||||
@ -222,7 +303,6 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
|
|
||||||
private _onIntervalChange(e: Event) {
|
private _onIntervalChange(e: Event) {
|
||||||
this._interval = (e.target! as any).value;
|
this._interval = (e.target! as any).value;
|
||||||
this._updateRule();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onRepeatSelected(e: CustomEvent<SelectedDetail<number>>) {
|
private _onRepeatSelected(e: CustomEvent<SelectedDetail<number>>) {
|
||||||
@ -233,9 +313,20 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
if (this._freq !== "weekly") {
|
if (this._freq !== "weekly") {
|
||||||
this._weekday.clear();
|
this._weekday.clear();
|
||||||
|
this._computeWeekday();
|
||||||
}
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this._updateRule();
|
}
|
||||||
|
|
||||||
|
private _onMonthlyDetailSelected(e: CustomEvent<SelectedDetail<number>>) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const selectedItem = this._monthlyRepeatItems[e.detail.index];
|
||||||
|
if (!selectedItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._monthlyRepeat = selectedItem.value;
|
||||||
|
this._monthlyRepeatWeekday = selectedItem.byday;
|
||||||
|
this._monthday = selectedItem.bymonthday;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onWeekdayToggle(e: MouseEvent) {
|
private _onWeekdayToggle(e: MouseEvent) {
|
||||||
@ -246,7 +337,6 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
this._weekday.delete(value);
|
this._weekday.delete(value);
|
||||||
}
|
}
|
||||||
this._updateRule();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onEndSelected(e: CustomEvent<SelectedDetail<number>>) {
|
private _onEndSelected(e: CustomEvent<SelectedDetail<number>>) {
|
||||||
@ -270,31 +360,47 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
this._until = undefined;
|
this._until = undefined;
|
||||||
}
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this._updateRule();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onCountChange(e: Event) {
|
private _onCountChange(e: Event) {
|
||||||
this._count = (e.target! as any).value;
|
this._count = (e.target! as any).value;
|
||||||
this._updateRule();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onUntilChange(e: CustomEvent) {
|
private _onUntilChange(e: CustomEvent) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this._until = new Date(e.detail.value);
|
this._until = new Date(e.detail.value);
|
||||||
this._updateRule();
|
}
|
||||||
|
|
||||||
|
// Reset the weekday selected when there is only a single value
|
||||||
|
private _computeWeekday() {
|
||||||
|
if (this.dtstart && this._weekday.size <= 1) {
|
||||||
|
const weekdayNum = getWeekday(this.dtstart);
|
||||||
|
this._weekday.clear();
|
||||||
|
this._weekday.add(new Weekday(weekdayNum).toString() as WeekdayStr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _computeRRule() {
|
private _computeRRule() {
|
||||||
if (this._freq === undefined || this._freq === "none") {
|
if (this._freq === undefined || this._freq === "none") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const options = {
|
let byweekday: Weekday[] | undefined;
|
||||||
|
let bymonthday: number | undefined;
|
||||||
|
if (this._freq === "monthly" && this._monthlyRepeatWeekday !== undefined) {
|
||||||
|
byweekday = [this._monthlyRepeatWeekday];
|
||||||
|
} else if (this._freq === "monthly" && this._monthday !== undefined) {
|
||||||
|
bymonthday = this._monthday;
|
||||||
|
} else if (this._freq === "weekly") {
|
||||||
|
byweekday = ruleByWeekDay(this._weekday);
|
||||||
|
}
|
||||||
|
const options: Partial<Options> = {
|
||||||
freq: convertRepeatFrequency(this._freq!)!,
|
freq: convertRepeatFrequency(this._freq!)!,
|
||||||
interval: this._interval > 1 ? this._interval : undefined,
|
interval: this._interval > 1 ? this._interval : undefined,
|
||||||
byweekday: ruleByWeekDay(this._weekday),
|
|
||||||
count: this._count,
|
count: this._count,
|
||||||
until: this._until,
|
until: this._until,
|
||||||
tzid: this.timezone,
|
tzid: this.timezone,
|
||||||
|
byweekday: byweekday,
|
||||||
|
bymonthday: bymonthday,
|
||||||
};
|
};
|
||||||
const contentline = RRule.optionsToString(options);
|
const contentline = RRule.optionsToString(options);
|
||||||
return contentline.slice(6); // Strip "RRULE:" prefix
|
return contentline.slice(6); // Strip "RRULE:" prefix
|
||||||
|
@ -1,8 +1,22 @@
|
|||||||
// Library for converting back and forth from values use by this webcomponent
|
// Library for converting back and forth from values use by this webcomponent
|
||||||
// and the values defined by rrule.js.
|
// and the values defined by rrule.js.
|
||||||
import { RRule, Frequency, Weekday } from "rrule";
|
import {
|
||||||
import type { WeekdayStr } from "rrule";
|
addDays,
|
||||||
import { addDays, addMonths, addWeeks, addYears } from "date-fns";
|
addMonths,
|
||||||
|
addWeeks,
|
||||||
|
addYears,
|
||||||
|
getDate,
|
||||||
|
getDay,
|
||||||
|
isLastDayOfMonth,
|
||||||
|
isSameMonth,
|
||||||
|
} from "date-fns";
|
||||||
|
import type { Options, WeekdayStr } from "rrule";
|
||||||
|
import { Frequency, RRule, Weekday } from "rrule";
|
||||||
|
import { formatDate } from "../../common/datetime/format_date";
|
||||||
|
import { capitalizeFirstLetter } from "../../common/string/capitalize-first-letter";
|
||||||
|
import { dayNames } from "../../common/translations/day_names";
|
||||||
|
import { monthNames } from "../../common/translations/month_names";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
export type RepeatFrequency =
|
export type RepeatFrequency =
|
||||||
| "none"
|
| "none"
|
||||||
@ -21,6 +35,13 @@ export const DEFAULT_COUNT = {
|
|||||||
daily: 30,
|
daily: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface MonthlyRepeatItem {
|
||||||
|
value: string;
|
||||||
|
byday?: Weekday;
|
||||||
|
bymonthday?: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function intervalSuffix(freq: RepeatFrequency) {
|
export function intervalSuffix(freq: RepeatFrequency) {
|
||||||
if (freq === "monthly") {
|
if (freq === "monthly") {
|
||||||
return "months";
|
return "months";
|
||||||
@ -101,7 +122,16 @@ export const WEEKDAYS = [
|
|||||||
RRule.SA,
|
RRule.SA,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getWeekdays(firstDay?: number) {
|
/** Return a weekday number compatible with rrule.js weekdays */
|
||||||
|
export function getWeekday(dtstart: Date): number {
|
||||||
|
let weekDay = getDay(dtstart) - 1;
|
||||||
|
if (weekDay < 0) {
|
||||||
|
weekDay += 7;
|
||||||
|
}
|
||||||
|
return weekDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeekdays(firstDay?: number): Weekday[] {
|
||||||
if (firstDay === undefined || firstDay === 0) {
|
if (firstDay === undefined || firstDay === 0) {
|
||||||
return WEEKDAYS;
|
return WEEKDAYS;
|
||||||
}
|
}
|
||||||
@ -114,9 +144,7 @@ export function getWeekdays(firstDay?: number) {
|
|||||||
return weekDays;
|
return weekDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ruleByWeekDay(
|
export function ruleByWeekDay(weekdays: Set<WeekdayStr>): Weekday[] {
|
||||||
weekdays: Set<WeekdayStr>
|
|
||||||
): Weekday[] | undefined {
|
|
||||||
return Array.from(weekdays).map((value: string) => {
|
return Array.from(weekdays).map((value: string) => {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "MO":
|
case "MO":
|
||||||
@ -138,3 +166,127 @@ export function ruleByWeekDay(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the recurrence options based on the day of the month. The
|
||||||
|
* return values are a Weekday object that represent a BYDAY for a
|
||||||
|
* particular week of the month like "first Saturday" or "last Friday".
|
||||||
|
*/
|
||||||
|
function getWeekydaysForMonth(dtstart: Date): Weekday[] {
|
||||||
|
const weekDay = getWeekday(dtstart);
|
||||||
|
const dayOfMonth = getDate(dtstart);
|
||||||
|
const nthWeekdayOfMonth = Math.floor((dayOfMonth - 1) / 7) + 1;
|
||||||
|
const isLastWeekday = !isSameMonth(dtstart, addDays(dtstart, 7));
|
||||||
|
const byweekdays: Weekday[] = [];
|
||||||
|
if (!isLastWeekday || dayOfMonth <= 28) {
|
||||||
|
byweekdays.push(new Weekday(weekDay, nthWeekdayOfMonth));
|
||||||
|
}
|
||||||
|
if (isLastWeekday) {
|
||||||
|
byweekdays.push(new Weekday(weekDay, -1));
|
||||||
|
}
|
||||||
|
return byweekdays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of repeat values available for the specified date.
|
||||||
|
*/
|
||||||
|
export function getMonthlyRepeatItems(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
interval: number,
|
||||||
|
dtstart: Date
|
||||||
|
): MonthlyRepeatItem[] {
|
||||||
|
const getLabel = (repeatValue: string) =>
|
||||||
|
renderRRuleAsText(hass, `FREQ=MONTHLY;INTERVAL=${interval};${repeatValue}`);
|
||||||
|
|
||||||
|
const result: MonthlyRepeatItem[] = [
|
||||||
|
// The default repeat rule is on day of month e.g. 3rd day of month
|
||||||
|
{
|
||||||
|
value: `BYMONTHDAY=${getDate(dtstart)}`,
|
||||||
|
label: getLabel(`BYMONTHDAY=${getDate(dtstart)}`)!,
|
||||||
|
},
|
||||||
|
// Additional optional rules based on the week of month e.g. 2nd sunday of month
|
||||||
|
...getWeekydaysForMonth(dtstart).map((item) => ({
|
||||||
|
value: `BYDAY=${item.toString()}`,
|
||||||
|
byday: item,
|
||||||
|
label: getLabel(`BYDAY=${item.toString()}`)!,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
if (isLastDayOfMonth(dtstart)) {
|
||||||
|
result.push({
|
||||||
|
value: "BYMONTHDAY=-1",
|
||||||
|
bymonthday: -1,
|
||||||
|
label: getLabel(`BYMONTHDAY=-1`)!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMonthlyRepeatWeekdayFromRule(
|
||||||
|
rrule: Partial<Options>
|
||||||
|
): Weekday | undefined {
|
||||||
|
if (rrule.freq !== Frequency.MONTHLY) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
rrule.byweekday &&
|
||||||
|
Array.isArray(rrule.byweekday) &&
|
||||||
|
rrule.byweekday.length === 1 &&
|
||||||
|
rrule.byweekday[0] instanceof Weekday
|
||||||
|
) {
|
||||||
|
return rrule.byweekday[0];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMonthdayRepeatFromRule(
|
||||||
|
rrule: Partial<Options>
|
||||||
|
): number | undefined {
|
||||||
|
if (rrule.freq !== Frequency.MONTHLY || !rrule.bymonthday) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (Array.isArray(rrule.bymonthday)) {
|
||||||
|
return rrule.bymonthday[0];
|
||||||
|
}
|
||||||
|
return rrule.bymonthday;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around RRule.toText that assists with translation.
|
||||||
|
*/
|
||||||
|
export function renderRRuleAsText(hass: HomeAssistant, value: string) {
|
||||||
|
const rule = RRule.fromString(`RRULE:${value}`);
|
||||||
|
if (!rule.isFullyConvertibleToText()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return capitalizeFirstLetter(
|
||||||
|
rule.toText(
|
||||||
|
(id: string | number | Weekday): string => {
|
||||||
|
if (typeof id === "string") {
|
||||||
|
return hass.localize(`ui.components.calendar.event.rrule.${id}`);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dayNames: dayNames(hass.locale),
|
||||||
|
monthNames: monthNames(hass.locale),
|
||||||
|
tokens: {},
|
||||||
|
},
|
||||||
|
// Format the date
|
||||||
|
(year: number, month: string, day: number): string => {
|
||||||
|
if (!year || !month || !day) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// Build date so we can then format it
|
||||||
|
const date = new Date();
|
||||||
|
date.setFullYear(year);
|
||||||
|
// As input we already get the localized month name, so we now unfortunately
|
||||||
|
// need to convert it back to something Date can work with. The already localized
|
||||||
|
// months names are a must in the RRule.Language structure (an empty string[] would
|
||||||
|
// mean we get undefined months input in this method here).
|
||||||
|
date.setMonth(monthNames(hass.locale).indexOf(month));
|
||||||
|
date.setDate(day);
|
||||||
|
return formatDate(date, hass.locale);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user