393 files
This commit is contained in:
+77
-82
@@ -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")) {
|
||||
|
||||
Reference in New Issue
Block a user