148 files
This commit is contained in:
1 parent
43021c1d3a
commit
94e9f32a43
148 files changed
+15528
-2013
No files matched your search
@@ -307,6 +307,11 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
// complete/archive). Keys are `${entry_id}:${task_id}`.
|
||||
@state() private _bulkMode = false;
|
||||
@state() private _bulkSelected = new Set<string>();
|
||||
/** #188: multi-select in the All-objects view (delete / archive many at once). */
|
||||
@state() private _objBulkMode = false;
|
||||
@state() private _objBulkSelected = new Set<string>();
|
||||
/** #188: the task selection bar's overflow menu (Move to another object…). */
|
||||
@state() private _bulkMenuOpen = false;
|
||||
// Virtualized dashboard task table (large installs): only rows in the
|
||||
// scroll window are in the DOM; spacers keep the scrollbar honest. The
|
||||
// window is recomputed from `.content` scroll/resize (rAF-throttled).
|
||||
@@ -1308,6 +1313,8 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
private _showAllObjects(): void {
|
||||
this._pushPanelState("all_objects");
|
||||
this._view = "all_objects";
|
||||
this._objBulkMode = false;
|
||||
this._objBulkSelected = new Set();
|
||||
this._selectedEntryId = null;
|
||||
this._selectedTaskId = null;
|
||||
this._scrollContentToTop();
|
||||
@@ -2003,6 +2010,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
|
||||
private _toggleBulkMode(): void {
|
||||
this._bulkMode = !this._bulkMode;
|
||||
this._bulkMenuOpen = false;
|
||||
if (!this._bulkMode) this._bulkSelected = new Set();
|
||||
}
|
||||
|
||||
@@ -2045,6 +2053,122 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
else this._showToast(doneMsg(ok));
|
||||
}
|
||||
|
||||
/** #188: move every selected task to one object (the single-task move's
|
||||
* prompt, then task/move per row; rows already there are skipped). */
|
||||
private async _bulkMove(rows: TaskRow[]): Promise<void> {
|
||||
const targets = this._objects
|
||||
.filter((o) => !o.object.archived_at)
|
||||
.sort((a, b) => (a.object.name || "").localeCompare(b.object.name || ""));
|
||||
if (!targets.length || this._bulkSelected.size === 0) return;
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceConfirmDialog>("maintenance-confirm-dialog");
|
||||
const result = await dlg?.prompt({
|
||||
title: t("move_task_title", this._lang),
|
||||
message: t("bulk_move_message", this._lang),
|
||||
confirmText: t("move_task_title", this._lang),
|
||||
inputLabel: t("move_task_target", this._lang),
|
||||
inputValue: targets[0].entry_id,
|
||||
options: targets.map((o) => ({ value: o.entry_id, label: o.object.name || o.entry_id })),
|
||||
});
|
||||
if (!result?.confirmed || !result.value) return;
|
||||
const target = result.value;
|
||||
await this._runBulk(
|
||||
rows.filter((r) => r.entry_id !== target),
|
||||
(row) => ({ type: "maintenance_supporter/task/move", entry_id: row.entry_id, task_id: row.task_id, target_entry_id: target }),
|
||||
(n) => t("bulk_moved", this._lang).replace("{n}", String(n)),
|
||||
);
|
||||
}
|
||||
|
||||
// ── #188: bulk select in the All-objects view ──────────────────────────
|
||||
private _toggleObjBulkMode(): void {
|
||||
this._objBulkMode = !this._objBulkMode;
|
||||
if (!this._objBulkMode) this._objBulkSelected = new Set();
|
||||
}
|
||||
|
||||
private _toggleObjBulk(entryId: string): void {
|
||||
const next = new Set(this._objBulkSelected);
|
||||
if (next.has(entryId)) next.delete(entryId);
|
||||
else next.add(entryId);
|
||||
this._objBulkSelected = next;
|
||||
}
|
||||
|
||||
private _objBulkSelectAll(objs: MaintenanceObjectResponse[]): void {
|
||||
const ids = objs.map((o) => o.entry_id);
|
||||
const all = ids.length > 0 && ids.every((id) => this._objBulkSelected.has(id));
|
||||
this._objBulkSelected = all ? new Set() : new Set(ids);
|
||||
}
|
||||
|
||||
/** One WS call per selected object, sequentially (config entries are
|
||||
* removed one at a time on the backend); the successful count is reported. */
|
||||
private async _runObjBulk(type: string, doneMsg: (n: number) => string, undo?: (ids: string[]) => Promise<void>): Promise<void> {
|
||||
const ids = [...this._objBulkSelected];
|
||||
if (ids.length === 0) return;
|
||||
this._actionLoading = true;
|
||||
const done: string[] = [];
|
||||
for (const id of ids) {
|
||||
try {
|
||||
await this.hass.connection.sendMessagePromise({ type, entry_id: id });
|
||||
done.push(id);
|
||||
} catch { /* keep going; report the successful count */ }
|
||||
}
|
||||
this._actionLoading = false;
|
||||
this._objBulkSelected = new Set();
|
||||
this._objBulkMode = false;
|
||||
await this._loadData();
|
||||
if (undo && done.length > 0) this._showUndoToast(doneMsg(done.length), () => { void undo(done); });
|
||||
else this._showToast(doneMsg(done.length));
|
||||
}
|
||||
|
||||
private async _objBulkDelete(): Promise<void> {
|
||||
const n = this._objBulkSelected.size;
|
||||
if (n === 0) return;
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceConfirmDialog>("maintenance-confirm-dialog");
|
||||
const ok = await dlg?.confirm({
|
||||
title: t("delete", this._lang),
|
||||
message: t("bulk_delete_objects_confirm", this._lang).replace("{n}", String(n)),
|
||||
confirmText: t("delete", this._lang),
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
await this._runObjBulk("maintenance_supporter/object/delete", (k) => t("bulk_objects_deleted", this._lang).replace("{n}", String(k)));
|
||||
}
|
||||
|
||||
private _objBulkArchive(): void {
|
||||
void this._runObjBulk(
|
||||
"maintenance_supporter/object/archive",
|
||||
(k) => t("bulk_objects_archived", this._lang).replace("{n}", String(k)),
|
||||
async (ids) => {
|
||||
for (const id of ids) {
|
||||
try {
|
||||
await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/object/unarchive", entry_id: id });
|
||||
} catch { /* best effort */ }
|
||||
}
|
||||
await this._loadData();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private _renderObjBulkBar(objs: MaintenanceObjectResponse[], L: string) {
|
||||
const n = this._objBulkSelected.size;
|
||||
const allSelected = objs.length > 0 && objs.every((o) => this._objBulkSelected.has(o.entry_id));
|
||||
return html`
|
||||
<div class="bulk-bar obj-bulk-bar">
|
||||
<label class="bulk-selectall">
|
||||
<input type="checkbox" .checked=${allSelected} @change=${() => this._objBulkSelectAll(objs)} />
|
||||
${t("bulk_select_all", L)}
|
||||
</label>
|
||||
<span class="bulk-count">${t("bulk_n_selected", L).replace("{n}", String(n))}</span>
|
||||
<span class="bulk-actions">
|
||||
<ha-button appearance="plain" class="obj-bulk-archive" .disabled=${n === 0 || this._actionLoading} @click=${() => this._objBulkArchive()}>
|
||||
<ha-icon icon="mdi:archive-outline"></ha-icon> ${t("archive", L)}
|
||||
</ha-button>
|
||||
<ha-button appearance="filled" variant="danger" class="obj-bulk-delete" .disabled=${n === 0 || this._actionLoading} @click=${() => this._objBulkDelete()}>
|
||||
<ha-icon icon="mdi:delete-outline"></ha-icon> ${t("delete", L)}
|
||||
</ha-button>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _bulkComplete(rows: TaskRow[]): void {
|
||||
void this._runBulk(
|
||||
rows,
|
||||
@@ -3311,6 +3435,17 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
@click=${() => this._bulkArchive(rows)}>
|
||||
<ha-icon icon="mdi:archive-outline"></ha-icon> ${t("archive", L)}
|
||||
</ha-button>
|
||||
<span class="bulk-more-wrapper">
|
||||
<ha-button appearance="plain" class="bulk-more" title=${t("more_actions", L)} aria-label=${t("more_actions", L)} .disabled=${n === 0 || this._actionLoading}
|
||||
@click=${(e: Event) => { e.stopPropagation(); this._bulkMenuOpen = !this._bulkMenuOpen; }}>
|
||||
<ha-icon icon="mdi:dots-vertical"></ha-icon>
|
||||
</ha-button>
|
||||
${this._bulkMenuOpen ? html`
|
||||
<div class="popup-menu" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="popup-menu-item bulk-move" @click=${() => { this._bulkMenuOpen = false; void this._bulkMove(rows); }}>${t("move_task", L)}</div>
|
||||
</div>
|
||||
` : nothing}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -3454,7 +3589,13 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
const renderObject = (obj: MaintenanceObjectResponse) => {
|
||||
const overdue = obj.tasks.some(t => t.status === "overdue" || t.status === "triggered");
|
||||
return html`
|
||||
<div class="object-card${overdue ? ' object-card-overdue' : ''}" @click=${() => this._showObject(obj.entry_id)}>
|
||||
<div class="object-card${overdue ? ' object-card-overdue' : ''}${this._objBulkMode ? ' selectable' : ''}${this._objBulkMode && this._objBulkSelected.has(obj.entry_id) ? ' bulk-selected' : ''}"
|
||||
@click=${() => (this._objBulkMode ? this._toggleObjBulk(obj.entry_id) : this._showObject(obj.entry_id))}>
|
||||
${this._objBulkMode
|
||||
? html`<label class="obj-bulk-check bulk-check" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<input type="checkbox" .checked=${this._objBulkSelected.has(obj.entry_id)} @change=${() => this._toggleObjBulk(obj.entry_id)} />
|
||||
</label>`
|
||||
: nothing}
|
||||
${overdue ? html`<span class="overdue-dot" title="${t("has_overdue", L)}"></span>` : nothing}
|
||||
<div class="object-card-header">
|
||||
<span class="object-card-name">${this._objRef(obj.object)}${obj.object.name}</span>
|
||||
@@ -3544,6 +3685,12 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<ha-button appearance="plain" @click=${() => this._exportObjectsCsv()}>
|
||||
<ha-icon icon="mdi:file-delimited-outline"></ha-icon> ${t("settings_export_csv", L)}
|
||||
</ha-button>
|
||||
${!isOperator ? html`
|
||||
<ha-button appearance="plain" class="bulk-toggle obj-bulk-toggle ${this._objBulkMode ? "active" : ""}" @click=${() => this._toggleObjBulkMode()}>
|
||||
<ha-icon icon="mdi:checkbox-multiple-marked-outline"></ha-icon>
|
||||
${this._objBulkMode ? t("cancel", L) : t("bulk_select", L)}
|
||||
</ha-button>
|
||||
` : nothing}
|
||||
${archivedObjCount > 0 ? html`
|
||||
<ha-button
|
||||
class="archived-toggle ${this._showArchived ? "active" : ""}"
|
||||
@@ -3554,6 +3701,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
</ha-button>
|
||||
` : nothing}
|
||||
</div>
|
||||
${this._objBulkMode ? this._renderObjBulkBar(sorted, L) : nothing}
|
||||
${tableMode
|
||||
? this._renderObjectsTable(sorted)
|
||||
: this._groupByMode === "area"
|
||||
@@ -3696,6 +3844,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<table class="objects-table">
|
||||
<thead>
|
||||
<tr>
|
||||
${this._objBulkMode ? html`<th class="oc-bulk"></th>` : nothing}
|
||||
${cols.map((key) => {
|
||||
const def = OBJECT_COLUMNS.find((c) => c.key === key);
|
||||
// The actions column header stays blank (icon-only cells).
|
||||
@@ -3706,7 +3855,12 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
</thead>
|
||||
<tbody>
|
||||
${objs.map((obj) => html`
|
||||
<tr class="objects-table-row" @click=${() => this._showObject(obj.entry_id)}>
|
||||
<tr class="objects-table-row${this._objBulkMode && this._objBulkSelected.has(obj.entry_id) ? ' bulk-selected' : ''}"
|
||||
@click=${() => (this._objBulkMode ? this._toggleObjBulk(obj.entry_id) : this._showObject(obj.entry_id))}>
|
||||
${this._objBulkMode
|
||||
? html`<td class="oc-bulk"><input type="checkbox" .checked=${this._objBulkSelected.has(obj.entry_id)}
|
||||
@click=${(e: Event) => e.stopPropagation()} @change=${() => this._toggleObjBulk(obj.entry_id)} /></td>`
|
||||
: nothing}
|
||||
${cols.map((key) => this._renderObjectCell(key, obj, L))}
|
||||
</tr>
|
||||
`)}
|
||||
|
||||
Reference in new issue
Block a user