From 06ffc560d533a0887c0d4dccf67e66c6f98adbce Mon Sep 17 00:00:00 2001 From: Raul Camacho Date: Fri, 19 Apr 2024 20:34:46 +0000 Subject: [PATCH] feat: add ability to upload calendar event information --- src/data/calendar.ts | 21 ++ .../calendar/dialog-import-calendar-events.ts | 188 ++++++++++++++++++ src/panels/calendar/ha-panel-calendar.ts | 16 +- .../show-dialog-import-calendar-events.ts | 20 ++ src/translations/en.json | 9 + 5 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/panels/calendar/dialog-import-calendar-events.ts create mode 100644 src/panels/calendar/show-dialog-import-calendar-events.ts diff --git a/src/data/calendar.ts b/src/data/calendar.ts index 85efa30686af..2fe617caef8e 100644 --- a/src/data/calendar.ts +++ b/src/data/calendar.ts @@ -191,3 +191,24 @@ export const deleteCalendarEvent = ( recurrence_id, recurrence_range, }); + +export const uploadCalendarFile = async ( + hass: HomeAssistant, + entityId: string, + file: File +) => { + const fd = new FormData(); + fd.append("file", file); + fd.append("entity_id", entityId); + const resp = await hass.fetchWithAuth("/api/calendars/import", { + method: "POST", + body: fd, + }); + if (resp.status === 413) { + throw new Error(`Uploaded file is too large (${file.name})`); + } else if (resp.status !== 200) { + throw new Error("Unknown error"); + } + const data = await resp.json(); + return data.file_id; +}; diff --git a/src/panels/calendar/dialog-import-calendar-events.ts b/src/panels/calendar/dialog-import-calendar-events.ts new file mode 100644 index 000000000000..cbd07c0bbb83 --- /dev/null +++ b/src/panels/calendar/dialog-import-calendar-events.ts @@ -0,0 +1,188 @@ +import "@material/mwc-button"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; +import { property, state } from "lit/decorators"; +import { mdiFileUpload } from "@mdi/js"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/entity/state-info"; +import "../../components/ha-alert"; +import "../../components/ha-date-input"; +import { createCloseHeading } from "../../components/ha-dialog"; +import "../../components/ha-time-input"; +import { haStyleDialog } from "../../resources/styles"; +import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; +import { HomeAssistant } from "../../types"; +import "../lovelace/components/hui-generic-entity-row"; +import { stopPropagation } from "../../common/dom/stop_propagation"; +import { ImportCalendarEventsDialogParams } from "./show-dialog-import-calendar-events"; +import { uploadCalendarFile } from "../../data/calendar"; + +class DialogImportCalendarEvents extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: ImportCalendarEventsDialogParams; + + @state() private _calendarFile?: File; + + @state() private _selectedCalendarEntityId: string = ""; + + @state() private _uploading = false; + + @state() private _error?: string; + + public async showDialog( + params: ImportCalendarEventsDialogParams + ): Promise { + this._params = params; + } + + private closeDialog(): void { + this._params = undefined; + this._selectedCalendarEntityId = ""; + this._calendarFile = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render() { + if (!this._params) { + return nothing; + } + return html` + +
+ ${this._error + ? html`${this._error}` + : ""} + + ${this._params.calendars.map( + (item) => html` + + ${item.name} + + ` + )} + + +
+ + ${this.hass.localize( + "ui.components.calendar.import_events.upload_button" + )} + +
+ `; + } + + private _setCalendar(ev): void { + const entityId = ev.target.value; + this._selectedCalendarEntityId = entityId; + } + + private async _setCalendarFile(ev) { + this._calendarFile = ev.detail.files![0]; + } + + private async _beginFileSubmit() { + this._uploading = true; + + try { + const fileId = await uploadCalendarFile( + this.hass, + this._selectedCalendarEntityId, + this._calendarFile! + ); + fireEvent(this, "value-changed", { value: fileId }); + } catch (err: any) { + showAlertDialog(this, { + text: this.hass.localize( + "ui.components.calendar.import_events.upload_failed", + { + reason: err.message || err, + } + ), + }); + } finally { + this._uploading = false; + this._calendarFile = undefined; + this.closeDialog(); + } + } + + private _handleFileCleared() { + this._calendarFile = undefined; + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + state-info { + line-height: 40px; + } + ha-svg-icon { + width: 40px; + margin-right: 8px; + margin-inline-end: 8px; + margin-inline-start: initial; + direction: var(--direction); + vertical-align: top; + } + ha-file-upload { + margin-top: 16px; + } + .buttons { + display: flex; + justify-content: flex-end; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-import-calendar-events": DialogImportCalendarEvents; + } +} + +customElements.define( + "dialog-import-calendar-events", + DialogImportCalendarEvents +); diff --git a/src/panels/calendar/ha-panel-calendar.ts b/src/panels/calendar/ha-panel-calendar.ts index 2b9dfa8c3df9..bab068252653 100644 --- a/src/panels/calendar/ha-panel-calendar.ts +++ b/src/panels/calendar/ha-panel-calendar.ts @@ -1,7 +1,7 @@ import { ResizeController } from "@lit-labs/observers/resize-controller"; import "@material/mwc-list"; import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; -import { mdiChevronDown, mdiPlus, mdiRefresh } from "@mdi/js"; +import { mdiChevronDown, mdiPlus, mdiRefresh, mdiUpload } from "@mdi/js"; import { CSSResultGroup, LitElement, @@ -20,6 +20,7 @@ import "../../components/ha-button"; import "../../components/ha-button-menu"; import "../../components/ha-card"; import "../../components/ha-check-list-item"; +import "../../components/ha-file-upload"; import "../../components/ha-icon-button"; import type { HaListItem } from "../../components/ha-list-item"; import "../../components/ha-menu-button"; @@ -34,6 +35,7 @@ import { } from "../../data/calendar"; import { fetchIntegrationManifest } from "../../data/integration"; import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow"; +import { showImportCalendarEventsDialog } from "./show-dialog-import-calendar-events"; import { haStyle } from "../../resources/styles"; import type { CalendarViewChanged, HomeAssistant } from "../../types"; import "./ha-full-calendar"; @@ -166,6 +168,14 @@ class PanelCalendar extends LitElement { : html`
${this.hass.localize("ui.components.calendar.my_calendars")}
`} + { + showImportCalendarEventsDialog(this, { calendars: this._calendars }); + } + private async _handleViewChanged( ev: HASSDomEvent ): Promise { diff --git a/src/panels/calendar/show-dialog-import-calendar-events.ts b/src/panels/calendar/show-dialog-import-calendar-events.ts new file mode 100644 index 000000000000..7e4c2668b621 --- /dev/null +++ b/src/panels/calendar/show-dialog-import-calendar-events.ts @@ -0,0 +1,20 @@ +import { fireEvent } from "../../common/dom/fire_event"; +import { Calendar } from "../../data/calendar"; + +export interface ImportCalendarEventsDialogParams { + calendars: Calendar[]; +} + +export const loadImportCalendarEventsDialog = () => + import("./dialog-import-calendar-events"); + +export const showImportCalendarEventsDialog = ( + element: HTMLElement, + detailParams: ImportCalendarEventsDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-import-calendar-events", + dialogImport: loadImportCalendarEventsDialog, + dialogParams: detailParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 6478c5a339f7..db1d2fb5c188 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -882,6 +882,15 @@ "create_calendar": "Create calendar", "today": "Today", "event_retrieval_error": "Could not retrieve events for calendars:", + "import_events": { + "label": "Import event information", + "select_label": "Add to calendar", + "supported_formats": "Supports iCal (.ics) format", + "title": "Import event information", + "unsupported_format": "Unsupported format, please choose an ICS file.", + "upload_failed": "Upload failed", + "upload_button": "Upload" + }, "event": { "add": "Add event", "delete": "Delete event",