393 files

This commit is contained in:
Home Assistant Version Control
2026-09-27 17:16:54 +00:00
parent b05b7a897e
commit 39d97ac2db
404 changed files with 54006 additions and 11034 deletions
@@ -12,8 +12,13 @@ import { LitElement, html, css, nothing } from "lit";
import { isSafeHttpUrl } from "../helpers/url";
import { renderNotesMarkdown } from "../helpers/notes-markdown";
import { property, state } from "lit/decorators.js";
import { t, STATUS_COLORS, langOf } from "../styles";
import { describeWsError } from "../ws-errors";
import { t, langOf } from "../styles";
import { runWs } from "../helpers/ws-run";
import { statusColor, statusKey, statusLabel } from "../renderers/status";
import { fetchSettingsOnce } from "../helpers/settings-cache";
import { canWrite, NO_DELEGATION, type WriteAccess } from "../helpers/permissions";
import { confirmAction } from "../helpers/confirm";
import { focusModalShell, modalShellStyles, renderModalShell } from "../helpers/modal-shell";
import type { HomeAssistant, MaintenanceObject, MaintenanceTask } from "../types";
interface ObjectFull {
@@ -30,6 +35,9 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
@state() private _data: ObjectFull | null = null;
@state() private _busy = false;
@state() private _error = "";
/** Operator-write delegation — Add task / Edit / Archive / Delete follow
* canWrite() like the panel (DRY audit 2026-09-26). */
@state() private _access: WriteAccess = NO_DELEGATION;
private get _lang(): string {
return langOf(this.hass);
@@ -39,7 +47,10 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
this._entryId = entryId;
this._error = "";
this._open = true;
await this._load();
await Promise.all([
this._load(),
fetchSettingsOnce(this.hass).then((s) => { this._access = s.access; }),
]);
}
public close(): void {
@@ -50,15 +61,21 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
private async _load(): Promise<void> {
if (!this._entryId) return;
try {
const r = await this.hass.connection.sendMessagePromise<ObjectFull>({
type: "maintenance_supporter/object",
entry_id: this._entryId,
});
this._data = r;
} catch (e) {
this._error = describeWsError(e, this._lang);
}
const r = await runWs<ObjectFull>(
this,
{ type: "maintenance_supporter/object", entry_id: this._entryId },
{ onError: (m) => { this._error = m; } },
);
if (r !== undefined) this._data = r;
}
/** helpers/ws-run runWs bound to this dialog's busy flag and error line. */
private _runWs(payload: Record<string, unknown>): Promise<unknown> {
this._error = "";
return runWs(this, payload, {
busy: (b) => { this._busy = b; },
onError: (m) => { this._error = m; },
});
}
private _onEditObject(): void {
@@ -79,58 +96,50 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
private async _onDelete(): Promise<void> {
if (!this._entryId || !this._data) return;
const confirmText = t("delete_object_confirm", this._lang)
|| `Delete "${this._data.object.name}" and all its tasks?`;
if (!window.confirm(confirmText)) return;
this._busy = true;
this._error = "";
try {
await this.hass.connection.sendMessagePromise({
type: "maintenance_supporter/object/delete",
entry_id: this._entryId,
});
this.dispatchEvent(
new CustomEvent("object-deleted", {
detail: { entry_id: this._entryId },
bubbles: true, composed: true,
}),
);
this.close();
} catch (e) {
this._error = describeWsError(e, this._lang);
} finally {
this._busy = false;
}
const ok = await confirmAction(this.hass, {
title: t("delete", this._lang),
message: t("delete_object_confirm", this._lang),
confirmText: t("delete", this._lang),
danger: true,
});
if (!ok) return;
const entryId = this._entryId;
if ((await this._runWs({ type: "maintenance_supporter/object/delete", entry_id: entryId })) === undefined) return;
this.dispatchEvent(
new CustomEvent("object-deleted", {
detail: { entry_id: entryId },
bubbles: true, composed: true,
}),
);
this.close();
}
private async _onArchiveObject(): Promise<void> {
if (!this._entryId || !this._data) return;
const archived = !!this._data.object.archived;
if (!archived) {
const confirmText = t("confirm_archive_object", this._lang);
if (!window.confirm(confirmText)) return;
}
this._busy = true;
this._error = "";
try {
await this.hass.connection.sendMessagePromise({
type: archived
? "maintenance_supporter/object/unarchive"
: "maintenance_supporter/object/archive",
entry_id: this._entryId,
const ok = await confirmAction(this.hass, {
title: t("archive_object", this._lang),
message: t("confirm_archive_object", this._lang),
confirmText: t("archive_object", this._lang),
});
this.dispatchEvent(
new CustomEvent("object-changed", {
detail: { entry_id: this._entryId },
bubbles: true, composed: true,
}),
);
this.close();
} catch (e) {
this._error = describeWsError(e, this._lang);
} finally {
this._busy = false;
if (!ok) return;
}
const entryId = this._entryId;
const res = await this._runWs({
type: archived
? "maintenance_supporter/object/unarchive"
: "maintenance_supporter/object/archive",
entry_id: entryId,
});
if (res === undefined) return;
this.dispatchEvent(
new CustomEvent("object-changed", {
detail: { entry_id: entryId },
bubbles: true, composed: true,
}),
);
this.close();
}
private _onTaskClick(taskId: string): void {
@@ -148,11 +157,9 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
const data = this._data;
const obj = data?.object;
const tasks = data?.tasks || [];
const isAdmin = (this.hass?.user?.is_admin ?? true) as boolean;
const writer = canWrite(this.hass?.user, this._access);
return html`
<div class="backdrop" @click=${this.close}></div>
<div class="dialog" role="dialog" aria-modal="true">
return renderModalShell(() => this.close(), html`
${data && obj
? html`
<div class="header">
@@ -173,9 +180,9 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
<div class="task-list">
${tasks.map((task) => html`
<div class="task-row" @click=${() => this._onTaskClick(task.id)}>
<span class="status-dot" style="background: ${STATUS_COLORS[task.status] || "#ccc"}"></span>
<span class="status-dot" style="background: ${statusColor(task)}"></span>
<span class="task-name">${task.name}</span>
<span class="task-status">${t(task.status || "ok", L)}</span>
<span class="task-status ${statusKey(task)}">${statusLabel(statusKey(task), L)}</span>
</div>
`)}
</div>
@@ -191,7 +198,7 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
`
: nothing}
${isAdmin
${writer
? html`
<div class="actions">
<button class="btn primary" @click=${this._onAddTask} ?disabled=${this._busy}>
@@ -215,8 +222,11 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
: nothing}
`
: html`<div class="loading">${t("loading", L)}</div>`}
</div>
`;
`);
}
protected updated(changed: Map<string, unknown>): void {
if (changed.has("_open") && this._open) focusModalShell(this.shadowRoot);
}
private _renderMetaRow(obj: MaintenanceObject) {
@@ -251,23 +261,8 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
`;
}
static styles = css`
static styles = [modalShellStyles, css`
:host { display: contents; }
.backdrop {
position: fixed; inset: 0; z-index: 100; background: rgba(0,0,0,0.5);
}
.dialog {
position: fixed; left: 50%; top: 50%;
transform: translate(-50%, -50%);
width: 95vw; max-width: 480px;
max-height: 92vh; overflow: auto;
background: var(--card-background-color, var(--ha-card-background, #1c1c1c));
color: var(--primary-text-color);
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.4);
padding: 20px; z-index: 101;
display: flex; flex-direction: column; gap: 14px;
}
.header { display: flex; flex-direction: column; gap: 6px; }
.title { font-size: 20px; font-weight: 600; }
.meta { display: flex; flex-direction: column; gap: 4px; padding-top: 4px; border-top: 1px solid var(--divider-color); }
@@ -311,7 +306,7 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
.btn ha-icon { --mdc-icon-size: 16px; }
.loading { padding: 24px; text-align: center; color: var(--secondary-text-color); }
.error { padding: 8px; border-radius: 6px; background: rgba(211,47,47,0.1); color: var(--error-color); font-size: 13px; }
`;
`];
}
if (!customElements.get("maintenance-object-quick-actions-dialog")) {