/** Dialog to save the current panel filters as a named view + manage views. * * "Views" are shared, named combinations of the task-list filters (status / * user / archived) plus sort + group-by. This dialog saves the panel's *current* * filter state under a name and lists existing views for deletion. Applying a * view is done from the toolbar dropdown, not here. * * Uses a native for the name — is not registered in the * custom-panel context, so it renders without its field (the documented trap). */ import { css, html, LitElement, nothing } from "lit"; import { property, state } from "lit/decorators.js"; import { t, ensureLocale } from "../styles"; import { describeWsError } from "../ws-errors"; import type { HomeAssistant, SavedView, SavedViewFilters } from "../types"; interface ViewsResponse { views: SavedView[]; } export class MaintenanceSavedViewsDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _open = false; @state() private _busy = false; @state() private _error = ""; @state() private _name = ""; @state() private _views: SavedView[] = []; private _filters: SavedViewFilters | null = null; private _localeReady = false; private get _lang(): string { return this.hass?.language || "en"; } updated(changed: Map): void { if (changed.has("hass") && this.hass && !this._localeReady) { this._localeReady = true; ensureLocale(this._lang).then(() => this.requestUpdate()); } } public async open(currentFilters: SavedViewFilters, views: SavedView[]): Promise { this._open = true; this._error = ""; this._name = ""; this._filters = currentFilters; this._views = views; } private _close(): void { this._open = false; } private _emitChanged(views: SavedView[]): void { this._views = views; this.dispatchEvent( new CustomEvent("saved-views-changed", { bubbles: true, composed: true, detail: { views } }), ); } private _save = async (): Promise => { const name = this._name.trim(); if (!name || this._busy || !this._filters) return; this._busy = true; this._error = ""; try { const res = await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/views/save", name, filters: this._filters, }); this._name = ""; this._emitChanged(res.views || []); } catch (e) { this._error = describeWsError(e, this._lang); } finally { this._busy = false; } }; private _delete = async (viewId: string): Promise => { if (this._busy) return; this._busy = true; this._error = ""; try { const res = await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/views/delete", view_id: viewId, }); this._emitChanged(res.views || []); } catch (e) { this._error = describeWsError(e, this._lang); } finally { this._busy = false; } }; render() { if (!this._open) return html``; const L = this._lang; return html`
e.stopPropagation()}>
${t("views_dialog_title", L)}
${t("views_dialog_hint", L)}
${this._error ? html`
${this._error}
` : nothing}
(this._name = (e.target as HTMLInputElement).value)} @keydown=${(e: KeyboardEvent) => { if (e.key === "Enter") this._save(); }} /> ${t("views_save_current", L)}
${this._views.length === 0 ? html`
${t("views_none_yet", L)}
` : html`
${this._views.map( (v) => html`
${v.name} this._delete(v.id)} >
`, )}
`}
${t("close", L)}
`; } static styles = css` .overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; } .card { background: var(--card-background-color, #fff); color: var(--primary-text-color); border-radius: 12px; padding: 20px; display: flex; flex-direction: column; gap: 12px; min-width: 340px; max-width: 480px; width: 90vw; max-height: 80vh; overflow: hidden; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); } .title { font-size: 18px; font-weight: 500; } .hint { color: var(--secondary-text-color); font-size: 13px; } .error { color: var(--error-color, #f44336); font-size: 13px; } .save-row { display: flex; gap: 8px; align-items: center; } .name-input { flex: 1; padding: 8px 10px; border: 1px solid var(--divider-color); border-radius: 6px; background: var(--card-background-color, #fff); color: var(--primary-text-color); font-size: 14px; } .empty { color: var(--secondary-text-color); font-size: 14px; padding: 8px 0; } .list { display: flex; flex-direction: column; gap: 6px; overflow-y: auto; max-height: 50vh; } .row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px 8px; border: 1px solid var(--divider-color); border-radius: 6px; } .row-name { font-size: 14px; font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 8px; } `; } if (!customElements.get("maintenance-saved-views-dialog")) { customElements.define("maintenance-saved-views-dialog", MaintenanceSavedViewsDialog); }