124 files

This commit is contained in:
Home Assistant Version Control committed 2026-08-31 15:02:40 +00:00
1 parent 33de328a33
commit 9742f385a3
124 files changed
+13907 -1376

No files matched your search

@@ -1,6 +1,6 @@
/** Maintenance Supporter Sidebar Panel. */
import { LitElement, html, nothing } from "lit";
import { LitElement, html, nothing, type TemplateResult } from "lit";
import { isSafeHttpUrl } from "./helpers/url";
import { applySubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge";
import { isStaleBundle } from "./helpers/bundle-version";
@@ -120,6 +120,12 @@ type GroupByMode = "none" | "area" | "group" | "user";
export class MaintenanceSupporterPanel extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
/** #145: the panel itself is tight (< 880 px) although the viewport is not
* `narrow` — an iPad in portrait with HA's sidebar docked leaves ~768 px.
* The wide row grid then cannot hold labelled action buttons, so the
* compact (icon-only) form applies here too. Reflected for the CSS. */
@property({ type: Boolean, reflect: true }) public tight = false;
private _tightObserver: ResizeObserver | null = null;
@property({ attribute: false }) public panel: Record<string, unknown> = {};
@state() private _objects: MaintenanceObjectResponse[] = [];
@@ -170,6 +176,10 @@ export class MaintenanceSupporterPanel extends LitElement {
// value in the task-create dialog so the Settings → General → "Default
// warning days" choice actually flows through to new tasks.
@state() private _defaultWarningDays = 7;
/** #145: global "Task row actions" style (buttons_compact | buttons | icons). */
@state() private _rowActionStyle = "buttons_compact";
/** #145: one-time "rows look different now" notice for existing installs. */
@state() private _rowActionNotice = false;
@state() private _actionLoading = false;
@state() private _moreMenuOpen = false;
@state() private _objMenuOpen = false;
@@ -320,6 +330,19 @@ export class MaintenanceSupporterPanel extends LitElement {
else window.setTimeout(kick, 1500);
window.addEventListener("popstate", this._popstateHandler);
window.addEventListener("keydown", this._paletteKeydown);
if (typeof ResizeObserver !== "undefined") {
this._tightObserver = new ResizeObserver((entries) => {
const w = entries[0]?.contentRect.width ?? 0;
// 1000, not 880: between ~880 and ~1000 the wide grid technically
// fits its column minimums, but the chips track shrinks below any
// readable width — first the chips painted over the type column
// (duty-rotation GIF, 2026-08-30), clipped they left ellipsis
// fragments. Below ~1000 the narrow layout is the honest choice;
// from there up the chips track keeps ≥150 px of slack.
if (w > 0) this.tight = w < 1000;
});
this._tightObserver.observe(this);
}
window.addEventListener("resize", this._onVirtualScroll, { passive: true });
// localStorage.getItem can THROW (Safari private mode / locked-down policies
// where storage access raises rather than returning null) — an unguarded
@@ -360,6 +383,8 @@ export class MaintenanceSupporterPanel extends LitElement {
super.disconnectedCallback();
window.removeEventListener("popstate", this._popstateHandler);
window.removeEventListener("keydown", this._paletteKeydown);
this._tightObserver?.disconnect();
this._tightObserver = null;
window.removeEventListener("resize", this._onVirtualScroll);
this.shadowRoot?.querySelector(".content")?.removeEventListener("scroll", this._onVirtualScroll);
this._virtScrollAttached = false;
@@ -517,7 +542,7 @@ export class MaintenanceSupporterPanel extends LitElement {
features: AdvancedFeatures;
admin_panel_user_ids?: string[];
operator_write_enabled?: boolean;
general?: { default_warning_days?: number };
general?: { default_warning_days?: number; row_action_style?: string; row_action_notice_pending?: boolean };
objects_table_columns?: string[];
};
this._features = sr.features;
@@ -527,6 +552,9 @@ export class MaintenanceSupporterPanel extends LitElement {
if (typeof dwd === "number" && dwd >= 0 && dwd <= 365) {
this._defaultWarningDays = dwd;
}
const ras = sr.general?.row_action_style;
this._rowActionStyle = ras === "icons" || ras === "buttons" ? ras : "buttons_compact";
this._rowActionNotice = sr.general?.row_action_notice_pending === true;
this._objectsTableColumns = sanitizeColumns(sr.objects_table_columns);
}
@@ -1995,6 +2023,20 @@ export class MaintenanceSupporterPanel extends LitElement {
</div>
`
: nothing}
${this._rowActionNotice && !!this.hass?.user?.is_admin
? html`
<div class="update-banner row-actions-banner" role="status">
<ha-icon icon="mdi:gesture-tap-button"></ha-icon>
<span>${t("row_actions_banner", this._lang)}</span>
<ha-button appearance="plain" @click=${() => this._dismissRowActionNotice(false)}>
${t("row_actions_keep", this._lang)}
</ha-button>
<ha-button appearance="filled" @click=${() => this._dismissRowActionNotice(true)}>
${t("row_actions_back", this._lang)}
</ha-button>
</div>
`
: nothing}
${this.narrow || this._view !== "overview" ? this._renderHeader() : nothing}
<div class="content">
${this._view === "overview"
@@ -2285,10 +2327,19 @@ export class MaintenanceSupporterPanel extends LitElement {
<div class="today-task">${row.task_name}</div>
<div class="today-object">${row.object_name} · ${formatDueDays(row.days_until_due, L)}</div>
</div>
<mwc-icon-button class="btn-complete" title="${t("complete", L)}"
@click=${(e: Event) => { e.stopPropagation(); this._openCompleteDialogForRow(row); }}>
<ha-icon icon="mdi:check"></ha-icon>
</mwc-icon-button>
${this._actionStyle() === "icons"
? html`
<mwc-icon-button class="btn-complete" title="${t("complete", L)}"
@click=${(e: Event) => { e.stopPropagation(); this._openCompleteDialogForRow(row); }}>
<ha-icon icon="mdi:check"></ha-icon>
</mwc-icon-button>`
: html`
<ha-button size="small" appearance="accent" variant="success" class="today-complete" title="${t("complete", L)}"
@click=${(e: Event) => { e.stopPropagation(); this._openCompleteDialogForRow(row); }}>
${this._actionStyle() === "buttons_compact" && (this.narrow || this.tight)
? html`<ha-icon icon="mdi:check"></ha-icon>`
: html`<ha-icon slot="start" icon="mdi:check"></ha-icon>${t("complete", L)}`}
</ha-button>`}
</div>
`)}
</div>
@@ -3215,18 +3266,74 @@ export class MaintenanceSupporterPanel extends LitElement {
: nothing}
${renderMiniSparkline(row, this._miniStatsData, this._lang)}
</span>
<span class="row-actions">
<mwc-icon-button class="btn-complete" title="${t("complete", L)}" @click=${(e: Event) => { e.stopPropagation(); this._openCompleteDialogForRow(row); }}>
<ha-icon icon="mdi:check"></ha-icon>
</mwc-icon-button>
<mwc-icon-button class="btn-skip" title="${t("skip", L)}" .disabled=${this._actionLoading} @click=${(e: Event) => { e.stopPropagation(); this._promptSkipTask(row.entry_id, row.task_id); }}>
<ha-icon icon="mdi:skip-next"></ha-icon>
</mwc-icon-button>
</span>
${this._renderRowActions(L, () => this._openCompleteDialogForRow(row), () => this._promptSkipTask(row.entry_id, row.task_id))}
</div>
`;
}
/** DESIGN PROTOTYPE (#145 wish 1): row actions as bare icons (current) or
* as labelled colour-coded buttons. Switch: localStorage "msp-action-style"
* = "buttons" — evaluation only, no settings plumbing yet. */
private _actionStyle(): string {
return this._rowActionStyle;
}
/** The #145 banner's two ways out: keep the buttons (just dismiss) or go
* back to icons (style + dismiss in one write). Admin-only, like every
* global setting. */
private async _dismissRowActionNotice(backToIcons: boolean): Promise<void> {
const settings: Record<string, unknown> = { row_action_notice_pending: false };
if (backToIcons) settings.row_action_style = "icons";
try {
await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/global/update", settings });
this._rowActionNotice = false;
if (backToIcons) this._rowActionStyle = "icons";
} catch (err) {
console.warn("[maintenance-supporter] row-action notice update failed", err);
}
}
/** #145: row actions as HA buttons — labelled (accent/success + plain, the
* task-detail pairing) or, for "buttons_compact" on narrow screens,
* icon-only — or the classic icon pair when the household chose "icons". */
private _renderRowActions(L: string, onComplete: () => void, onSkip: () => void): TemplateResult {
const style = this._actionStyle();
if (style === "buttons" || style === "buttons_compact") {
const iconOnly = style === "buttons_compact" && (this.narrow || this.tight);
if (iconOnly) {
// Compact narrow form: the primary action stays a solid ha-button
// (icon only), the secondary one becomes HA's own icon button.
return html`
<span class="row-actions as-buttons compact">
<ha-button size="small" appearance="accent" variant="success" title="${t("complete", L)}" @click=${(e: Event) => { e.stopPropagation(); onComplete(); }}>
<ha-icon icon="mdi:check"></ha-icon>
</ha-button>
<ha-icon-button .label=${t("skip", L)} title="${t("skip", L)}" .disabled=${this._actionLoading} @click=${(e: Event) => { e.stopPropagation(); onSkip(); }}>
<ha-icon icon="mdi:skip-next"></ha-icon>
</ha-icon-button>
</span>`;
}
return html`
<span class="row-actions as-buttons">
<ha-button size="small" appearance="accent" variant="success" title="${t("complete", L)}" @click=${(e: Event) => { e.stopPropagation(); onComplete(); }}>
<ha-icon slot="start" icon="mdi:check"></ha-icon>${t("complete", L)}
</ha-button>
<ha-button size="small" appearance="plain" variant="neutral" title="${t("skip", L)}" ?disabled=${this._actionLoading} @click=${(e: Event) => { e.stopPropagation(); onSkip(); }}>
<ha-icon slot="start" icon="mdi:skip-next"></ha-icon>${t("skip", L)}
</ha-button>
</span>`;
}
return html`
<span class="row-actions">
<mwc-icon-button class="btn-complete" title="${t("complete", L)}" @click=${(e: Event) => { e.stopPropagation(); onComplete(); }}>
<ha-icon icon="mdi:check"></ha-icon>
</mwc-icon-button>
<mwc-icon-button class="btn-skip" title="${t("skip", L)}" .disabled=${this._actionLoading} @click=${(e: Event) => { e.stopPropagation(); onSkip(); }}>
<ha-icon icon="mdi:skip-next"></ha-icon>
</mwc-icon-button>
</span>`;
}
private _openCompleteDialogForRow(row: TaskRow): void {
const obj = this._objects.find(o => o.entry_id === row.entry_id);
const task = obj?.tasks.find(t => t.id === row.task_id);
@@ -3367,14 +3474,11 @@ export class MaintenanceSupporterPanel extends LitElement {
: nothing}
${renderMiniSparkline(task, this._miniStatsData, this._lang)}
</span>
<span class="row-actions">
<mwc-icon-button class="btn-complete" title="${t("complete", L)}" @click=${(e: Event) => { e.stopPropagation(); this._openCompleteDialog(obj.entry_id, task.id, task.name, this._features.checklists ? task.checklist : undefined, this._features.adaptive && !!task.adaptive_config?.enabled); }}>
<ha-icon icon="mdi:check"></ha-icon>
</mwc-icon-button>
<mwc-icon-button class="btn-skip" title="${t("skip", L)}" .disabled=${this._actionLoading} @click=${(e: Event) => { e.stopPropagation(); this._promptSkipTask(obj.entry_id, task.id); }}>
<ha-icon icon="mdi:skip-next"></ha-icon>
</mwc-icon-button>
</span>
${this._renderRowActions(
L,
() => this._openCompleteDialog(obj.entry_id, task.id, task.name, this._features.checklists ? task.checklist : undefined, this._features.adaptive && !!task.adaptive_config?.enabled),
() => this._promptSkipTask(obj.entry_id, task.id),
)}
</div>
`)}</div>`}