diff --git a/src/components/ha-form/compute-initial-ha-form-data.ts b/src/components/ha-form/compute-initial-ha-form-data.ts index 6d24bbe531..96c1ae58fb 100644 --- a/src/components/ha-form/compute-initial-ha-form-data.ts +++ b/src/components/ha-form/compute-initial-ha-form-data.ts @@ -50,6 +50,7 @@ export const computeInitialHaFormData = ( "text" in selector || "addon" in selector || "attribute" in selector || + "file" in selector || "icon" in selector || "theme" in selector ) { diff --git a/src/components/ha-selector/ha-selector-file.ts b/src/components/ha-selector/ha-selector-file.ts new file mode 100644 index 0000000000..589e43cf9d --- /dev/null +++ b/src/components/ha-selector/ha-selector-file.ts @@ -0,0 +1,98 @@ +import { mdiFile } from "@mdi/js"; +import { html, LitElement, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import { removeFile, uploadFile } from "../../data/file_upload"; +import { FileSelector } from "../../data/selector"; +import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; +import { HomeAssistant } from "../../types"; +import "../ha-file-upload"; + +@customElement("ha-selector-file") +export class HaFileSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: FileSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property() public helper?: string; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + @state() private _filename?: { fileId: string; name: string }; + + @state() private _busy = false; + + protected render() { + return html` + + `; + } + + protected willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + if ( + changedProps.has("value") && + this._filename && + this.value !== this._filename.fileId + ) { + this._filename = undefined; + } + } + + private async _uploadFile(ev) { + this._busy = true; + + const file = ev.detail.files![0]; + + try { + const fileId = await uploadFile(this.hass, file); + this._filename = { fileId, name: file.name }; + fireEvent(this, "value-changed", { value: fileId }); + } catch (err: any) { + showAlertDialog(this, { + text: this.hass.localize("ui.components.selectors.file.upload_failed", { + reason: err.message || err, + }), + }); + } finally { + this._busy = false; + } + } + + private _removeFile = async () => { + this._busy = true; + try { + await removeFile(this.hass, this.value!); + } catch (err) { + // Not ideal if removal fails, but will be cleaned up later + } finally { + this._busy = false; + } + this._filename = undefined; + fireEvent(this, "value-changed", { value: "" }); + }; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-file": HaFileSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index e2b3fdbf97..7d6400b289 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -14,6 +14,7 @@ import "./ha-selector-datetime"; import "./ha-selector-device"; import "./ha-selector-duration"; import "./ha-selector-entity"; +import "./ha-selector-file"; import "./ha-selector-number"; import "./ha-selector-object"; import "./ha-selector-select"; diff --git a/src/data/file_upload.ts b/src/data/file_upload.ts new file mode 100644 index 0000000000..8ee9a4393b --- /dev/null +++ b/src/data/file_upload.ts @@ -0,0 +1,22 @@ +import { HomeAssistant } from "../types"; + +export const uploadFile = async (hass: HomeAssistant, file: File) => { + const fd = new FormData(); + fd.append("file", file); + const resp = await hass.fetchWithAuth("/api/file_upload", { + 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; +}; + +export const removeFile = async (hass: HomeAssistant, file_id: string) => + hass.callApi("DELETE", "file_upload", { + file_id, + }); diff --git a/src/data/selector.ts b/src/data/selector.ts index b56f43d901..86d6d0eda9 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -16,6 +16,7 @@ export type Selector = | DeviceSelector | DurationSelector | EntitySelector + | FileSelector | IconSelector | LocationSelector | MediaSelector @@ -120,6 +121,12 @@ export interface EntitySelector { }; } +export interface FileSelector { + file: { + accept: string; + }; +} + export interface IconSelector { icon: { placeholder?: string;