393 files
This commit is contained in:
+49
-59
@@ -10,8 +10,11 @@ import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, ensureLocale, langOf } from "../styles";
|
||||
import { registerCustomCard } from "../helpers/register-card";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { runWs } from "../helpers/ws-run";
|
||||
import { sectionCardSharedStyles } from "./section-card-shared-styles";
|
||||
import { fetchSettingsOnce } from "../helpers/settings-cache";
|
||||
import { canWrite, NO_DELEGATION, type WriteAccess } from "../helpers/permissions";
|
||||
import { confirmAction } from "../helpers/confirm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
interface GroupEntry {
|
||||
@@ -36,6 +39,8 @@ export class MaintenanceGroupsSectionCard extends LitElement {
|
||||
@state() private _newName = "";
|
||||
@state() private _editingId: string | null = null;
|
||||
@state() private _editingName = "";
|
||||
/** Operator-write delegation — group CRUD is write tier (canWrite). */
|
||||
@state() private _access: WriteAccess = NO_DELEGATION;
|
||||
|
||||
private _hasInitiallyLoaded = false;
|
||||
|
||||
@@ -51,8 +56,12 @@ export class MaintenanceGroupsSectionCard extends LitElement {
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
private get _isAdmin(): boolean {
|
||||
return (this.hass?.user?.is_admin ?? true) as boolean;
|
||||
/** Group create / rename / delete are WRITE tier on the server: admins,
|
||||
* plus allowlisted operators with delegation on — the panel's rule
|
||||
* (helpers/permissions). `is_admin` alone hid the controls from a
|
||||
* delegated operator (DRY audit 2026-09-26). */
|
||||
private get _canWrite(): boolean {
|
||||
return canWrite(this.hass?.user, this._access);
|
||||
}
|
||||
|
||||
updated(changedProps: Map<string, unknown>): void {
|
||||
@@ -60,40 +69,39 @@ export class MaintenanceGroupsSectionCard extends LitElement {
|
||||
if (changedProps.has("hass") && this.hass && !this._hasInitiallyLoaded) {
|
||||
this._hasInitiallyLoaded = true;
|
||||
void this._load();
|
||||
void fetchSettingsOnce(this.hass).then((s) => { this._access = s.access; });
|
||||
void ensureLocale(this._lang).then(() => this.requestUpdate());
|
||||
}
|
||||
}
|
||||
|
||||
private async _load(): Promise<void> {
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<GroupsResp>({
|
||||
type: "maintenance_supporter/groups",
|
||||
});
|
||||
this._groups = r.groups || {};
|
||||
this._loaded = true;
|
||||
} catch (e) {
|
||||
this._error = describeWsError(e, this._lang);
|
||||
}
|
||||
const r = await runWs<GroupsResp>(this, { type: "maintenance_supporter/groups" }, {
|
||||
onError: (m) => { this._error = m; },
|
||||
});
|
||||
if (r === undefined) return;
|
||||
this._groups = r?.groups || {};
|
||||
this._loaded = true;
|
||||
}
|
||||
|
||||
/** One group mutation — helpers/ws-run runWs with the busy flag, the
|
||||
* error line and the list reload (`onDone` runs first). */
|
||||
private async _act(payload: Record<string, unknown>, onDone?: () => void): Promise<void> {
|
||||
this._error = "";
|
||||
await runWs(this, payload, {
|
||||
busy: (b) => { this._busy = b; },
|
||||
reload: async () => {
|
||||
onDone?.();
|
||||
await this._load();
|
||||
},
|
||||
onError: (m) => { this._error = m; },
|
||||
});
|
||||
}
|
||||
|
||||
private async _addGroup(): Promise<void> {
|
||||
if (!this._isAdmin) return;
|
||||
if (!this._canWrite) return;
|
||||
const name = this._newName.trim();
|
||||
if (!name) return;
|
||||
this._busy = true;
|
||||
this._error = "";
|
||||
try {
|
||||
await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/group/create",
|
||||
name,
|
||||
});
|
||||
this._newName = "";
|
||||
await this._load();
|
||||
} catch (e) {
|
||||
this._error = describeWsError(e, this._lang);
|
||||
} finally {
|
||||
this._busy = false;
|
||||
}
|
||||
await this._act({ type: "maintenance_supporter/group/create", name }, () => { this._newName = ""; });
|
||||
}
|
||||
|
||||
private _startEdit(id: string): void {
|
||||
@@ -102,43 +110,25 @@ export class MaintenanceGroupsSectionCard extends LitElement {
|
||||
}
|
||||
|
||||
private async _saveEdit(): Promise<void> {
|
||||
if (!this._isAdmin || !this._editingId) return;
|
||||
if (!this._canWrite || !this._editingId) return;
|
||||
const name = this._editingName.trim();
|
||||
if (!name) return;
|
||||
this._busy = true;
|
||||
this._error = "";
|
||||
try {
|
||||
await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/group/update",
|
||||
group_id: this._editingId,
|
||||
name,
|
||||
});
|
||||
await this._act({ type: "maintenance_supporter/group/update", group_id: this._editingId, name }, () => {
|
||||
this._editingId = null;
|
||||
this._editingName = "";
|
||||
await this._load();
|
||||
} catch (e) {
|
||||
this._error = describeWsError(e, this._lang);
|
||||
} finally {
|
||||
this._busy = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _deleteGroup(id: string, name: string): Promise<void> {
|
||||
if (!this._isAdmin) return;
|
||||
const confirmText = t("group_delete_confirm", this._lang).replace("{name}", name);
|
||||
if (!window.confirm(confirmText)) return;
|
||||
this._busy = true;
|
||||
try {
|
||||
await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/group/delete",
|
||||
group_id: id,
|
||||
});
|
||||
await this._load();
|
||||
} catch (e) {
|
||||
this._error = describeWsError(e, this._lang);
|
||||
} finally {
|
||||
this._busy = false;
|
||||
}
|
||||
if (!this._canWrite) return;
|
||||
const ok = await confirmAction(this.hass, {
|
||||
title: t("delete", this._lang),
|
||||
message: t("delete_group_confirm", this._lang).replace("{name}", name),
|
||||
confirmText: t("delete", this._lang),
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
await this._act({ type: "maintenance_supporter/group/delete", group_id: id });
|
||||
}
|
||||
|
||||
private _onDeepLink(): void {
|
||||
@@ -210,7 +200,7 @@ export class MaintenanceGroupsSectionCard extends LitElement {
|
||||
: html`
|
||||
<span class="group-name">${g.name || "Unnamed"}</span>
|
||||
<span class="task-count">${taskCount}</span>
|
||||
${this._isAdmin
|
||||
${this._canWrite
|
||||
? html`
|
||||
<button class="icon-btn"
|
||||
title="${t("edit", L)}"
|
||||
@@ -233,7 +223,7 @@ export class MaintenanceGroupsSectionCard extends LitElement {
|
||||
</div>
|
||||
`}
|
||||
|
||||
${this._isAdmin
|
||||
${this._canWrite
|
||||
? html`
|
||||
<div class="add-row">
|
||||
<input type="text"
|
||||
|
||||
Reference in New Issue
Block a user