/** Card editor for the Maintenance Supporter Lovelace card. */ import { LitElement, html, css, nothing } from "lit"; import { property, state } from "lit/decorators.js"; import { t } from "./styles"; import type { HomeAssistant, CardConfig, MaintenanceObjectResponse, SavedView } from "./types"; const STATUS_KEYS = ["overdue", "triggered", "due_soon", "ok"] as const; export class MaintenanceSupporterCardEditor extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config: CardConfig = { type: "custom:maintenance-supporter-card" }; @state() private _objects: MaintenanceObjectResponse[] = []; @state() private _loadingObjects = true; @state() private _loadError = false; @state() private _views: SavedView[] = []; private _objectsLoaded = false; private get _lang(): string { return this.hass?.language || "en"; } setConfig(config: CardConfig): void { this._config = { ...config }; } updated(changedProps: Map): void { super.updated(changedProps); if (changedProps.has("hass") && this.hass && !this._objectsLoaded) { this._objectsLoaded = true; this._loadObjects(); } } private async _loadObjects(): Promise { try { const result = await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/objects", }) as { objects: MaintenanceObjectResponse[] }; this._objects = result.objects || []; this._loadError = false; } catch { this._objects = []; this._loadError = true; } this._loadingObjects = false; // Saved views feed the scope dropdown; best-effort — the editor works // without them (the dropdown just stays at "none"). try { const res = await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/views/list", }) as { views: SavedView[] }; this._views = res.views || []; } catch { this._views = []; } } private _valueChanged(key: string, value: unknown): void { const newConfig = { ...this._config, [key]: value }; // Drop empty arrays / empty strings so the saved YAML stays clean if ((Array.isArray(value) && value.length === 0) || value === "") { delete (newConfig as Record)[key]; } this._config = newConfig; this.dispatchEvent( new CustomEvent("config-changed", { detail: { config: newConfig } }) ); } private _toggleStatus(status: string, on: boolean): void { const current = new Set(this._config.filter_status || []); if (on) current.add(status); else current.delete(status); this._valueChanged("filter_status", [...current]); } private _toggleObject(name: string, on: boolean): void { const current = new Set(this._config.filter_objects || []); if (on) current.add(name); else current.delete(name); this._valueChanged("filter_objects", [...current]); } private _onEntitiesChanged = (e: CustomEvent<{ value: string[] }>): void => { this._valueChanged("entity_ids", e.detail.value || []); }; render() { const L = this._lang; const selectedStatuses = new Set(this._config.filter_status || []); const selectedObjects = new Set(this._config.filter_objects || []); const objectNames = [...this._objects] .map((o) => o.object.name) .sort((a, b) => a.localeCompare(b)); // Build the list of OUR sensor + binary_sensor entity_ids so the // ha-entities-picker only shows maintenance_supporter entities, not // every sensor in HA. const ourEntities: string[] = []; for (const o of this._objects) { for (const t of o.tasks) { if (t.sensor_entity_id) ourEntities.push(t.sensor_entity_id); if (t.binary_sensor_entity_id) ourEntities.push(t.binary_sensor_entity_id); } } return html`
this._valueChanged("title", (e.target as HTMLInputElement).value)} >
${t("card_filter_status", L)}
${STATUS_KEYS.map((s) => html` `)}
${t("card_filter_status_help", L)}
${t("card_filter_objects", L)}
${this._loadingObjects ? html`
${t("card_loading_objects", L)}
` : this._loadError ? html`
${t("card_load_error", L)}
` : objectNames.length === 0 ? html`
${t("no_objects", L)}
` : html`
${objectNames.map((name) => html` `)}
${t("card_filter_objects_help", L)}
` }
${t("card_filter_entities", L)}
${t("card_filter_entities_help", L)}
${this._views.length > 0 ? html`
${t("card_saved_view", L)}
${t("card_saved_view_help", L)}
` : nothing} this._valueChanged("show_header", (e.target as HTMLInputElement).checked)} > this._valueChanged("show_actions", (e.target as HTMLInputElement).checked)} > this._valueChanged("show_assignee", (e.target as HTMLInputElement).checked)} > this._valueChanged("compact", (e.target as HTMLInputElement).checked)} > this._valueChanged("max_items", parseInt((e.target as HTMLInputElement).value, 10) || 0)} > ${nothing}
`; } static styles = css` .editor { display: flex; flex-direction: column; gap: 16px; padding: 16px; } ha-textfield { display: block; } .field { display: flex; flex-direction: column; gap: 6px; } .field-label { font-size: 13px; color: var(--secondary-text-color); font-weight: 500; } .field-help { font-size: 12px; color: var(--secondary-text-color); font-style: italic; } .field-help.error-text { color: var(--error-color, #f44336); font-style: normal; } .chip-row { display: flex; gap: 6px; flex-wrap: wrap; } .chip { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border: 1px solid var(--divider-color); border-radius: 14px; cursor: pointer; font-size: 13px; user-select: none; transition: background 0.15s, border-color 0.15s; } .chip:hover { background: var(--secondary-background-color); } .chip.active { background: var(--primary-color); color: var(--text-primary-color); border-color: var(--primary-color); } .chip input { display: none; } .object-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 4px 12px; padding: 6px 0; } .object-row { display: flex; align-items: center; gap: 6px; padding: 3px 0; font-size: 13px; cursor: pointer; } .object-row input { cursor: pointer; } .view-select { padding: 8px; border: 1px solid var(--divider-color); border-radius: 4px; background: var(--card-background-color, #fff); color: var(--primary-text-color); font-size: 14px; max-width: 320px; } `; } // Module-bottom registration so esbuild's tree-shaker doesn't drop the // element class when the only reference looks "type-only" (the // @customElement decorator pattern triggered exactly that bug for the // dialog components — same fix applied here for the card editor). if (!customElements.get("maintenance-supporter-card-editor")) { customElements.define( "maintenance-supporter-card-editor", MaintenanceSupporterCardEditor, ); }