From 7d28f3f5853f26d235d363a9d5c04495a7c8d2ed Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 26 Jun 2024 11:51:32 +0200 Subject: [PATCH] Allow to hide and sort columns in data tables (#21168) * Allow to hide and sort columns in data tables * fix unused * store --- .../data-table/dialog-data-table-settings.ts | 280 ++++++++++++++++++ src/components/data-table/ha-data-table.ts | 110 ++++++- .../show-dialog-data-table-settings.ts | 26 ++ src/layouts/hass-tabs-subpage-data-table.ts | 41 ++- .../config/automation/ha-automation-picker.ts | 61 ++-- src/panels/config/backup/ha-config-backup.ts | 17 +- .../config/blueprint/ha-blueprint-overview.ts | 41 ++- .../devices/ha-config-devices-dashboard.ts | 221 +++++++------- .../config/entities/ha-config-entities.ts | 53 ++-- .../config/helpers/ha-config-helpers.ts | 52 ++-- src/panels/config/labels/ha-config-labels.ts | 28 ++ .../ha-config-lovelace-dashboards.ts | 157 +++++----- .../resources/ha-config-lovelace-resources.ts | 23 ++ src/panels/config/scene/ha-scene-dashboard.ts | 41 ++- src/panels/config/script/ha-script-picker.ts | 71 ++--- src/panels/config/tags/ha-config-tags.ts | 155 +++++----- src/panels/config/users/ha-config-users.ts | 37 ++- .../ha-config-voice-assistants-expose.ts | 50 +++- src/translations/en.json | 15 +- 19 files changed, 1043 insertions(+), 436 deletions(-) create mode 100644 src/components/data-table/dialog-data-table-settings.ts create mode 100644 src/components/data-table/show-dialog-data-table-settings.ts diff --git a/src/components/data-table/dialog-data-table-settings.ts b/src/components/data-table/dialog-data-table-settings.ts new file mode 100644 index 0000000000..265abe0da0 --- /dev/null +++ b/src/components/data-table/dialog-data-table-settings.ts @@ -0,0 +1,280 @@ +import "@material/mwc-list"; +import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { repeat } from "lit/directives/repeat"; +import memoizeOne from "memoize-one"; +import { haStyleDialog } from "../../resources/styles"; +import { HomeAssistant } from "../../types"; +import { createCloseHeading } from "../ha-dialog"; +import "../ha-list-item"; +import "../ha-sortable"; +import "../ha-button"; +import { DataTableColumnContainer, DataTableColumnData } from "./ha-data-table"; +import { DataTableSettingsDialogParams } from "./show-dialog-data-table-settings"; +import { fireEvent } from "../../common/dom/fire_event"; + +@customElement("dialog-data-table-settings") +export class DialogDataTableSettings extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: DataTableSettingsDialogParams; + + @state() private _columnOrder?: string[]; + + @state() private _hiddenColumns?: string[]; + + public showDialog(params: DataTableSettingsDialogParams) { + this._params = params; + this._columnOrder = params.columnOrder; + this._hiddenColumns = params.hiddenColumns; + } + + public closeDialog() { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + private _sortedColumns = memoizeOne( + ( + columns: DataTableColumnContainer, + columnOrder: string[] | undefined, + hiddenColumns: string[] | undefined + ) => + Object.keys(columns) + .filter((col) => !columns[col].hidden) + .sort((a, b) => { + const orderA = columnOrder?.indexOf(a) ?? -1; + const orderB = columnOrder?.indexOf(b) ?? -1; + const hiddenA = + hiddenColumns?.includes(a) ?? Boolean(columns[a].defaultHidden); + const hiddenB = + hiddenColumns?.includes(b) ?? Boolean(columns[b].defaultHidden); + if (hiddenA !== hiddenB) { + return hiddenA ? 1 : -1; + } + if (orderA !== orderB) { + if (orderA === -1) { + return 1; + } + if (orderB === -1) { + return -1; + } + } + return orderA - orderB; + }) + .reduce( + (arr, key) => { + arr.push({ key, ...columns[key] }); + return arr; + }, + [] as (DataTableColumnData & { key: string })[] + ) + ); + + protected render() { + if (!this._params) { + return nothing; + } + + const columns = this._sortedColumns( + this._params.columns, + this._columnOrder, + this._hiddenColumns + ); + + return html` + + + + ${repeat( + columns, + (col) => col.key, + (col, _idx) => { + const canMove = !col.main && col.moveable !== false; + const canHide = !col.main && col.hideable !== false; + const isVisible = !(this._columnOrder && + this._columnOrder.includes(col.key) + ? this._hiddenColumns?.includes(col.key) ?? col.defaultHidden + : col.defaultHidden); + + return html`${col.title || col.label || col.key} + ${canMove && isVisible + ? html`` + : nothing} + + `; + } + )} + + + ${this.hass.localize( + "ui.components.data-table.settings.restore" + )} + + ${this.hass.localize("ui.components.data-table.settings.done")} + + + `; + } + + private _columnMoved(ev: CustomEvent): void { + ev.stopPropagation(); + if (!this._params) { + return; + } + const { oldIndex, newIndex } = ev.detail; + + const columns = this._sortedColumns( + this._params.columns, + this._columnOrder, + this._hiddenColumns + ); + + const columnOrder = columns.map((column) => column.key); + + const option = columnOrder.splice(oldIndex, 1)[0]; + columnOrder.splice(newIndex, 0, option); + + this._columnOrder = columnOrder; + + this._params!.onUpdate(this._columnOrder, this._hiddenColumns); + } + + _toggle(ev) { + if (!this._params) { + return; + } + const column = ev.target.column; + const wasHidden = ev.target.hidden; + + const hidden = [ + ...(this._hiddenColumns ?? + Object.entries(this._params.columns) + .filter(([_key, col]) => col.defaultHidden) + .map(([key]) => key)), + ]; + if (wasHidden && hidden.includes(column)) { + hidden.splice(hidden.indexOf(column), 1); + } else if (!wasHidden) { + hidden.push(column); + } + + const columns = this._sortedColumns( + this._params.columns, + this._columnOrder, + this._hiddenColumns + ); + + if (!this._columnOrder) { + this._columnOrder = columns.map((col) => col.key); + } else { + columns.forEach((col) => { + if (!this._columnOrder!.includes(col.key)) { + this._columnOrder!.push(col.key); + if (col.defaultHidden) { + hidden.push(col.key); + } + } + }); + } + + this._hiddenColumns = hidden; + + this._params!.onUpdate(this._columnOrder, this._hiddenColumns); + } + + _reset() { + this._columnOrder = undefined; + this._hiddenColumns = undefined; + + this._params!.onUpdate(this._columnOrder, this._hiddenColumns); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-dialog { + --mdc-dialog-max-width: 500px; + --dialog-z-index: 10; + --dialog-content-padding: 0 8px; + } + @media all and (max-width: 451px) { + ha-dialog { + --vertical-align-dialog: flex-start; + --dialog-surface-margin-top: 250px; + --ha-dialog-border-radius: 28px 28px 0 0; + --mdc-dialog-min-height: calc(100% - 250px); + --mdc-dialog-max-height: calc(100% - 250px); + } + } + ha-list-item { + --mdc-list-side-padding: 12px; + overflow: visible; + } + .hidden { + color: var(--disabled-text-color); + } + .handle { + cursor: move; /* fallback if grab cursor is unsupported */ + cursor: grab; + } + .actions { + display: flex; + flex-direction: row; + } + ha-icon-button { + display: block; + margin: -12px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-data-table-settings": DialogDataTableSettings; + } +} diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index f40c4a9046..bd1507e2ee 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -65,6 +65,10 @@ export interface DataTableSortColumnData { valueColumn?: string; direction?: SortingDirection; groupable?: boolean; + moveable?: boolean; + hideable?: boolean; + defaultHidden?: boolean; + showNarrow?: boolean; } export interface DataTableColumnData extends DataTableSortColumnData { @@ -79,6 +83,7 @@ export interface DataTableColumnData extends DataTableSortColumnData { | "overflow-menu" | "flex"; template?: (row: T) => TemplateResult | string | typeof nothing; + extraTemplate?: (row: T) => TemplateResult | string | typeof nothing; width?: string; maxWidth?: string; grows?: boolean; @@ -105,6 +110,8 @@ const UNDEFINED_GROUP_KEY = "zzzzz_undefined"; export class HaDataTable extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @property({ type: Boolean }) public narrow = false; + @property({ type: Object }) public columns: DataTableColumnContainer = {}; @property({ type: Array }) public data: DataTableRowData[] = []; @@ -145,6 +152,10 @@ export class HaDataTable extends LitElement { @property({ attribute: false }) public initialCollapsedGroups?: string[]; + @property({ attribute: false }) public hiddenColumns?: string[]; + + @property({ attribute: false }) public columnOrder?: string[]; + @state() private _filterable = false; @state() private _filter = ""; @@ -235,6 +246,7 @@ export class HaDataTable extends LitElement { (column: ClonedDataTableColumnData) => { delete column.title; delete column.template; + delete column.extraTemplate; } ); @@ -272,12 +284,44 @@ export class HaDataTable extends LitElement { this._sortFilterData(); } - if (properties.has("selectable")) { + if (properties.has("selectable") || properties.has("hiddenColumns")) { this._items = [...this._items]; } } + private _sortedColumns = memoizeOne( + (columns: DataTableColumnContainer, columnOrder?: string[]) => { + if (!columnOrder || !columnOrder.length) { + return columns; + } + + return Object.keys(columns) + .sort((a, b) => { + const orderA = columnOrder!.indexOf(a); + const orderB = columnOrder!.indexOf(b); + if (orderA !== orderB) { + if (orderA === -1) { + return 1; + } + if (orderB === -1) { + return -1; + } + } + return orderA - orderB; + }) + .reduce((obj, key) => { + obj[key] = columns[key]; + return obj; + }, {}) as DataTableColumnContainer; + } + ); + protected render() { + const columns = this._sortedColumns(this.columns, this.columnOrder); + + const renderRow = (row: DataTableRowData, index: number) => + this._renderRow(columns, this.narrow, row, index); + return html`
@@ -326,9 +370,14 @@ export class HaDataTable extends LitElement {
` : ""} - ${Object.entries(this.columns).map(([key, column]) => { - if (column.hidden) { - return ""; + ${Object.entries(columns).map(([key, column]) => { + if ( + column.hidden || + (this.columnOrder && this.columnOrder.includes(key) + ? this.hiddenColumns?.includes(key) ?? column.defaultHidden + : column.defaultHidden) + ) { + return nothing; } const sorted = key === this.sortColumn; const classes = { @@ -399,7 +448,7 @@ export class HaDataTable extends LitElement { @scroll=${this._saveScrollPos} .items=${this._items} .keyFunction=${this._keyFunction} - .renderItem=${this._renderRow} + .renderItem=${renderRow} > `} @@ -409,7 +458,12 @@ export class HaDataTable extends LitElement { private _keyFunction = (row: DataTableRowData) => row?.[this.id] || row; - private _renderRow = (row: DataTableRowData, index: number) => { + private _renderRow = ( + columns: DataTableColumnContainer, + narrow: boolean, + row: DataTableRowData, + index: number + ) => { // not sure how this happens... if (!row) { return nothing; @@ -454,8 +508,14 @@ export class HaDataTable extends LitElement { ` : ""} - ${Object.entries(this.columns).map(([key, column]) => { - if (column.hidden) { + ${Object.entries(columns).map(([key, column]) => { + if ( + (narrow && !column.main && !column.showNarrow) || + column.hidden || + (this.columnOrder && this.columnOrder.includes(key) + ? this.hiddenColumns?.includes(key) ?? column.defaultHidden + : column.defaultHidden) + ) { return nothing; } return html` @@ -482,7 +542,38 @@ export class HaDataTable extends LitElement { }) : ""} > - ${column.template ? column.template(row) : row[key]} + ${column.template + ? column.template(row) + : narrow && column.main + ? html`
${row[key]}
+
+ ${Object.entries(columns) + .filter( + ([key2, column2]) => + !column2.hidden && + !column2.main && + !column2.showNarrow && + !(this.columnOrder && + this.columnOrder.includes(key2) + ? this.hiddenColumns?.includes(key2) ?? + column2.defaultHidden + : column2.defaultHidden) + ) + .map( + ([key2, column2], i) => + html`${i !== 0 + ? " ⸱ " + : nothing}${column2.template + ? column2.template(row) + : row[key2]}` + )} +
+ ${column.extraTemplate + ? column.extraTemplate(row) + : nothing}` + : html`${row[key]}${column.extraTemplate + ? column.extraTemplate(row) + : nothing}`} `; })} @@ -861,6 +952,7 @@ export class HaDataTable extends LitElement { width: 100%; border: 0; white-space: nowrap; + position: relative; } .mdc-data-table__cell { diff --git a/src/components/data-table/show-dialog-data-table-settings.ts b/src/components/data-table/show-dialog-data-table-settings.ts new file mode 100644 index 0000000000..b31e801acb --- /dev/null +++ b/src/components/data-table/show-dialog-data-table-settings.ts @@ -0,0 +1,26 @@ +import { fireEvent } from "../../common/dom/fire_event"; +import { DataTableColumnContainer } from "./ha-data-table"; + +export interface DataTableSettingsDialogParams { + columns: DataTableColumnContainer; + onUpdate: ( + columnOrder: string[] | undefined, + hiddenColumns: string[] | undefined + ) => void; + hiddenColumns?: string[]; + columnOrder?: string[]; +} + +export const loadDataTableSettingsDialog = () => + import("./dialog-data-table-settings"); + +export const showDataTableSettingsDialog = ( + element: HTMLElement, + dialogParams: DataTableSettingsDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-data-table-settings", + dialogImport: loadDataTableSettingsDialog, + dialogParams, + }); +}; diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index f59c569c8c..4f062182a2 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -6,6 +6,7 @@ import { mdiArrowDown, mdiArrowUp, mdiClose, + mdiCog, mdiFilterVariant, mdiFilterVariantRemove, mdiFormatListChecks, @@ -42,6 +43,7 @@ import "../components/search-input-outlined"; import type { HomeAssistant, Route } from "../types"; import "./hass-tabs-subpage"; import type { PageNavigation } from "./hass-tabs-subpage"; +import { showDataTableSettingsDialog } from "../components/data-table/show-dialog-data-table-settings"; @customElement("hass-tabs-subpage-data-table") export class HaTabsSubpageDataTable extends LitElement { @@ -171,6 +173,10 @@ export class HaTabsSubpageDataTable extends LitElement { @property({ attribute: false }) public groupOrder?: string[]; + @property({ attribute: false }) public columnOrder?: string[]; + + @property({ attribute: false }) public hiddenColumns?: string[]; + @state() private _sortColumn?: string; @state() private _sortDirection: SortingDirection = null; @@ -290,6 +296,14 @@ export class HaTabsSubpageDataTable extends LitElement { ` : nothing; + const settingsButton = html` + + `; + return html` ${!this.narrow ? html` @@ -438,7 +455,7 @@ export class HaTabsSubpageDataTable extends LitElement {
${this.hasFilters && !this.showFilters ? html`${filterButton}` - : nothing}${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu} + : nothing}${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}${settingsButton}
@@ -448,7 +465,7 @@ export class HaTabsSubpageDataTable extends LitElement { ${this.hasFilters && !this.showFilters ? html`${filterButton}` : nothing} - ${selectModeBtn}${groupByMenu}${sortByMenu} + ${selectModeBtn}${groupByMenu}${sortByMenu}${settingsButton} `} `}
@@ -608,6 +625,22 @@ export class HaTabsSubpageDataTable extends LitElement { fireEvent(this, "grouping-changed", { value: columnId }); } + private _openSettings() { + showDataTableSettingsDialog(this, { + columns: this.columns, + hiddenColumns: this.hiddenColumns, + columnOrder: this.columnOrder, + onUpdate: ( + columnOrder: string[] | undefined, + hiddenColumns: string[] | undefined + ) => { + this.columnOrder = columnOrder; + this.hiddenColumns = hiddenColumns; + fireEvent(this, "columns-changed", { columnOrder, hiddenColumns }); + }, + }); + } + private _collapseAllGroups() { this._dataTable.collapseAllGroups(); } @@ -874,6 +907,10 @@ declare global { interface HASSDomEvents { "search-changed": { value: string }; "grouping-changed": { value: string }; + "columns-changed": { + columnOrder: string[] | undefined; + hiddenColumns: string[] | undefined; + }; "clear-filter": undefined; } } diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 0418f9b701..67da2d580d 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -192,6 +192,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { }) private _activeCollapsed?: string; + @storage({ + key: "automation-table-column-order", + state: false, + subscribe: false, + }) + private _activeColumnOrder?: string[]; + + @storage({ + key: "automation-table-hidden-columns", + state: false, + subscribe: false, + }) + private _activeHiddenColumns?: string[]; + @query("#overflow-menu") private _overflowMenu!: HaMenu; private _sizeController = new ResizeController(this, { @@ -253,6 +267,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { title: "", label: localize("ui.panel.config.automation.picker.headers.state"), type: "icon", + moveable: false, + showNarrow: true, template: (automation) => html` { - const date = new Date(automation.attributes.last_triggered); - const now = new Date(); - const dayDifference = differenceInDays(now, date); - return html` -
${automation.name}
- ${narrow - ? html`
- ${this.hass.localize("ui.card.automation.last_triggered")}: - ${automation.attributes.last_triggered - ? dayDifference > 3 - ? formatShortDateTime(date, locale, this.hass.config) - : relativeTime(date, locale) - : localize("ui.components.relative_time.never")} -
` - : nothing} - ${automation.labels.length - ? html`` - : nothing} - `; - }, + extraTemplate: (automation) => + automation.labels.length + ? html`` + : nothing, }, area: { title: localize("ui.panel.config.automation.picker.headers.area"), @@ -322,7 +321,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { sortable: true, width: "130px", title: localize("ui.card.automation.last_triggered"), - hidden: narrow, template: (automation) => { if (!automation.last_triggered) { return this.hass.localize("ui.components.relative_time.never"); @@ -341,9 +339,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { width: "82px", sortable: true, groupable: true, + hidden: narrow, title: "", type: "overflow", - hidden: narrow, label: this.hass.localize("ui.panel.config.automation.picker.state"), template: (automation) => html` html` - html`${backup.name} -
${backup.path}
`, + template: narrow + ? undefined + : (backup) => + html`${backup.name} +
${backup.path}
`, + }, + path: { + title: localize("ui.panel.config.backup.path"), + hidden: !narrow, }, size: { title: localize("ui.panel.config.backup.size"), width: "15%", - hidden: narrow, filterable: true, sortable: true, template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB", @@ -76,7 +81,6 @@ class HaConfigBackup extends LitElement { title: localize("ui.panel.config.backup.created"), width: "15%", direction: "desc", - hidden: narrow, filterable: true, sortable: true, template: (backup) => @@ -87,6 +91,9 @@ class HaConfigBackup extends LitElement { title: "", width: "15%", type: "overflow-menu", + showNarrow: true, + hideable: false, + moveable: false, template: (backup) => html` => ({ name: { @@ -165,19 +177,12 @@ class HaBlueprintOverview extends LitElement { filterable: true, direction: "asc", grows: true, - template: narrow - ? (blueprint) => html` - ${blueprint.name}
-
${blueprint.path}
- ` - : undefined, }, translated_type: { title: localize("ui.panel.config.blueprint.overview.headers.type"), sortable: true, filterable: true, groupable: true, - hidden: narrow, direction: "asc", width: "10%", }, @@ -185,7 +190,6 @@ class HaBlueprintOverview extends LitElement { title: localize("ui.panel.config.blueprint.overview.headers.file_name"), sortable: true, filterable: true, - hidden: narrow, direction: "asc", width: "25%", }, @@ -197,6 +201,9 @@ class HaBlueprintOverview extends LitElement { title: "", width: this.narrow ? undefined : "10%", type: "overflow-menu", + showNarrow: true, + moveable: false, + hideable: false, template: (blueprint) => blueprint.error ? html` entries[0]?.contentRect.width, }); @@ -434,10 +448,13 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { typeof this._devicesAndFilterDomains >["devicesOutput"][number]; - const columns: DataTableColumnContainer = { + return { icon: { title: "", + label: localize("ui.panel.config.devices.data_table.icon"), type: "icon", + moveable: false, + showNarrow: true, template: (device) => device.domains.length ? html`` : "", }, - }; - - if (narrow) { - columns.name = { + name: { title: localize("ui.panel.config.devices.data_table.device"), main: true, sortable: true, filterable: true, direction: "asc", grows: true, - template: (device) => html` -
${device.name}
-
${device.area} | ${device.integration}
+ extraTemplate: (device) => html` ${device.label_entries.length ? html` html` -
${device.name}
- ${device.label_entries.length - ? html` - - ` - : nothing} - `, - }; - } - - columns.manufacturer = { - title: localize("ui.panel.config.devices.data_table.manufacturer"), - sortable: true, - hidden: narrow, - filterable: true, - groupable: true, - width: "15%", - }; - columns.model = { - title: localize("ui.panel.config.devices.data_table.model"), - sortable: true, - hidden: narrow, - filterable: true, - width: "15%", - }; - columns.area = { - title: localize("ui.panel.config.devices.data_table.area"), - sortable: true, - hidden: narrow, - filterable: true, - groupable: true, - width: "15%", - }; - columns.integration = { - title: localize("ui.panel.config.devices.data_table.integration"), - sortable: true, - hidden: narrow, - filterable: true, - groupable: true, - width: "15%", - }; - columns.battery_entity = { - title: localize("ui.panel.config.devices.data_table.battery"), - sortable: true, - filterable: true, - type: "numeric", - width: narrow ? "105px" : "15%", - maxWidth: "105px", - valueColumn: "battery_level", - template: (device) => { - const batteryEntityPair = device.battery_entity; - const battery = - batteryEntityPair && batteryEntityPair[0] - ? this.hass.states[batteryEntityPair[0]] - : undefined; - const batteryDomain = battery ? computeStateDomain(battery) : undefined; - const batteryCharging = - batteryEntityPair && batteryEntityPair[1] - ? this.hass.states[batteryEntityPair[1]] - : undefined; - - return battery && - (batteryDomain === "binary_sensor" || !isNaN(battery.state as any)) - ? html` - ${batteryDomain === "sensor" - ? this.hass.formatEntityState(battery) - : nothing} - - ` - : html`—`; }, - }; - columns.disabled_by = { - title: "", - label: localize("ui.panel.config.devices.data_table.disabled_by"), - hidden: true, - template: (device) => - device.disabled_by - ? this.hass.localize("ui.panel.config.devices.disabled") - : "", - }; - columns.labels = { - title: "", - hidden: true, - filterable: true, - template: (device) => - device.label_entries.map((lbl) => lbl.name).join(" "), - }; + manufacturer: { + title: localize("ui.panel.config.devices.data_table.manufacturer"), + sortable: true, + filterable: true, + groupable: true, + width: "15%", + }, + model: { + title: localize("ui.panel.config.devices.data_table.model"), + sortable: true, + filterable: true, + width: "15%", + }, + area: { + title: localize("ui.panel.config.devices.data_table.area"), + sortable: true, + filterable: true, + groupable: true, + width: "15%", + }, + integration: { + title: localize("ui.panel.config.devices.data_table.integration"), + sortable: true, + filterable: true, + groupable: true, + width: "15%", + }, + battery_entity: { + title: localize("ui.panel.config.devices.data_table.battery"), + showNarrow: true, + sortable: true, + filterable: true, + type: "numeric", + width: narrow ? "105px" : "15%", + maxWidth: "105px", + valueColumn: "battery_level", + template: (device) => { + const batteryEntityPair = device.battery_entity; + const battery = + batteryEntityPair && batteryEntityPair[0] + ? this.hass.states[batteryEntityPair[0]] + : undefined; + const batteryDomain = battery + ? computeStateDomain(battery) + : undefined; + const batteryCharging = + batteryEntityPair && batteryEntityPair[1] + ? this.hass.states[batteryEntityPair[1]] + : undefined; - return columns; + return battery && + (batteryDomain === "binary_sensor" || !isNaN(battery.state as any)) + ? html` + ${batteryDomain === "sensor" + ? this.hass.formatEntityState(battery) + : nothing} + + ` + : html`—`; + }, + }, + disabled_by: { + title: "", + label: localize("ui.panel.config.devices.data_table.disabled_by"), + hidden: true, + template: (device) => + device.disabled_by + ? this.hass.localize("ui.panel.config.devices.disabled") + : "", + }, + labels: { + title: "", + hidden: true, + filterable: true, + template: (device) => + device.label_entries.map((lbl) => lbl.name).join(" "), + }, + } as DataTableColumnContainer; }); protected hassSubscribe(): (UnsubscribeFunc | Promise)[] { @@ -704,6 +693,9 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { .initialGroupColumn=${this._activeGrouping} .initialCollapsedGroups=${this._activeCollapsed} .initialSorting=${this._activeSorting} + .columnOrder=${this._activeColumnOrder} + .hiddenColumns=${this._activeHiddenColumns} + @columns-changed=${this._handleColumnsChanged} @clear-filter=${this._clearFilter} @search-changed=${this._handleSearchChange} @sorting-changed=${this._handleSortingChanged} @@ -1043,6 +1035,11 @@ ${rejected this._activeCollapsed = ev.detail.value; } + private _handleColumnsChanged(ev: CustomEvent) { + this._activeColumnOrder = ev.detail.columnOrder; + this._activeHiddenColumns = ev.detail.hiddenColumns; + } + static get styles(): CSSResultGroup { return [ css` diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 0eeeac1a51..971f810533 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -186,6 +186,20 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { }) private _activeCollapsed?: string; + @storage({ + key: "entities-table-column-order", + state: false, + subscribe: false, + }) + private _activeColumnOrder?: string[]; + + @storage({ + key: "entities-table-hidden-columns", + state: false, + subscribe: false, + }) + private _activeHiddenColumns?: string[]; + @query("hass-tabs-subpage-data-table", true) private _dataTable!: HaTabsSubpageDataTable; @@ -251,15 +265,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ]); private _columns = memoize( - ( - localize: LocalizeFunc, - narrow, - _language - ): DataTableColumnContainer => ({ + (localize: LocalizeFunc): DataTableColumnContainer => ({ icon: { title: "", label: localize("ui.panel.config.entities.picker.headers.state_icon"), type: "icon", + showNarrow: true, + moveable: false, template: (entry) => entry.icon ? html`` @@ -283,32 +295,23 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { filterable: true, direction: "asc", grows: true, - template: (entry) => html` -
${entry.name}
- ${narrow - ? html`
- ${entry.entity_id} | ${entry.localized_platform} -
` - : nothing} - ${entry.label_entries.length + extraTemplate: (entry) => + entry.label_entries.length ? html` ` - : nothing} - `, + : nothing, }, entity_id: { title: localize("ui.panel.config.entities.picker.headers.entity_id"), - hidden: narrow, sortable: true, filterable: true, width: "25%", }, localized_platform: { title: localize("ui.panel.config.entities.picker.headers.integration"), - hidden: narrow, sortable: true, groupable: true, filterable: true, @@ -324,7 +327,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { area: { title: localize("ui.panel.config.entities.picker.headers.area"), sortable: true, - hidden: narrow, filterable: true, groupable: true, width: "15%", @@ -343,6 +345,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { status: { title: localize("ui.panel.config.entities.picker.headers.status"), type: "icon", + showNarrow: true, sortable: true, filterable: true, width: "68px", @@ -688,11 +691,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { } .route=${this.route} .tabs=${configSections.devices} - .columns=${this._columns( - this.hass.localize, - this.narrow, - this.hass.language - )} + .columns=${this._columns(this.hass.localize)} .data=${filteredEntities} .searchLabel=${this.hass.localize( "ui.panel.config.entities.picker.search", @@ -714,6 +713,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { .initialGroupColumn=${this._activeGrouping} .initialCollapsedGroups=${this._activeCollapsed} .initialSorting=${this._activeSorting} + .columnOrder=${this._activeColumnOrder} + .hiddenColumns=${this._activeHiddenColumns} + @columns-changed=${this._handleColumnsChanged} @sorting-changed=${this._handleSortingChanged} @grouping-changed=${this._handleGroupingChanged} @collapsed-changed=${this._handleCollapseChanged} @@ -1335,6 +1337,11 @@ ${rejected this._activeCollapsed = ev.detail.value; } + private _handleColumnsChanged(ev: CustomEvent) { + this._activeColumnOrder = ev.detail.columnOrder; + this._activeHiddenColumns = ev.detail.hiddenColumns; + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index 032eb362f6..1046866244 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -167,6 +167,20 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { }) private _filter = ""; + @storage({ + key: "helpers-table-column-order", + state: false, + subscribe: false, + }) + private _activeColumnOrder?: string[]; + + @storage({ + key: "helpers-table-hidden-columns", + state: false, + subscribe: false, + }) + private _activeHiddenColumns?: string[]; + @state() private _stateItems: HassEntity[] = []; @state() private _entityEntries?: Record; @@ -243,14 +257,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { } private _columns = memoizeOne( - ( - narrow: boolean, - localize: LocalizeFunc - ): DataTableColumnContainer => ({ + (localize: LocalizeFunc): DataTableColumnContainer => ({ icon: { title: "", label: localize("ui.panel.config.helpers.picker.headers.icon"), type: "icon", + showNarrow: true, + moveable: false, template: (helper) => helper.entity ? html` html` -
${helper.name}
- ${narrow - ? html`
${helper.entity_id}
` - : nothing} - ${helper.label_entries.length + extraTemplate: (helper) => + helper.label_entries.length ? html` ` - : nothing} - `, + : nothing, }, entity_id: { title: localize("ui.panel.config.helpers.picker.headers.entity_id"), - hidden: this.narrow, sortable: true, filterable: true, width: "25%", @@ -313,10 +320,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { }, editable: { title: "", - label: this.hass.localize( - "ui.panel.config.helpers.picker.headers.editable" - ), + label: localize("ui.panel.config.helpers.picker.headers.editable"), type: "icon", + showNarrow: true, template: (helper) => html` ${!helper.editable ? html` @@ -337,8 +343,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { }, actions: { title: "", + label: "Actions", width: "64px", type: "overflow-menu", + hideable: false, + moveable: false, + showNarrow: true, template: (helper) => html` { const columns: DataTableColumnContainer = { icon: { title: "", + moveable: false, + showNarrow: true, label: localize("ui.panel.config.labels.headers.icon"), type: "icon", template: (label) => @@ -77,6 +93,7 @@ export class HaConfigLabels extends LitElement { }, color: { title: "", + showNarrow: true, label: localize("ui.panel.config.labels.headers.color"), type: "icon", template: (label) => @@ -105,6 +122,9 @@ export class HaConfigLabels extends LitElement { }, actions: { title: "", + showNarrow: true, + moveable: false, + hideable: false, width: "64px", type: "overflow-menu", template: (label) => html` @@ -167,6 +187,9 @@ export class HaConfigLabels extends LitElement { .noDataText=${this.hass.localize("ui.panel.config.labels.no_labels")} hasFab .initialSorting=${this._activeSorting} + .columnOrder=${this._activeColumnOrder} + .hiddenColumns=${this._activeHiddenColumns} + @columns-changed=${this._handleColumnsChanged} @sorting-changed=${this._handleSortingChanged} .filter=${this._filter} @search-changed=${this._handleSearchChange} @@ -297,6 +320,11 @@ export class HaConfigLabels extends LitElement { private _handleSearchChange(ev: CustomEvent) { this._filter = ev.detail.value; } + + private _handleColumnsChanged(ev: CustomEvent) { + this._activeColumnOrder = ev.detail.columnOrder; + this._activeHiddenColumns = ev.detail.hiddenColumns; + } } declare global { diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index 15df18b002..9952ba17a9 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -85,6 +85,20 @@ export class HaConfigLovelaceDashboards extends LitElement { }) private _activeSorting?: SortingChangedEvent; + @storage({ + key: "lovelace-dashboards-table-column-order", + state: false, + subscribe: false, + }) + private _activeColumnOrder?: string[]; + + @storage({ + key: "lovelace-dashboards-table-hidden-columns", + state: false, + subscribe: false, + }) + private _activeHiddenColumns?: string[]; + public willUpdate() { if (!this.hasUpdated) { this.hass.loadFragmentTranslation("lovelace"); @@ -101,6 +115,8 @@ export class HaConfigLovelaceDashboards extends LitElement { const columns: DataTableColumnContainer = { icon: { title: "", + moveable: false, + showNarrow: true, label: localize( "ui.panel.config.lovelace.dashboards.picker.headers.icon" ), @@ -128,87 +144,75 @@ export class HaConfigLovelaceDashboards extends LitElement { sortable: true, filterable: true, grows: true, - template: (dashboard) => { - const titleTemplate = html` - ${dashboard.title} - ${dashboard.default - ? html` - - - ${this.hass.localize( - `ui.panel.config.lovelace.dashboards.default_dashboard` - )} - - ` - : ""} - `; - return narrow - ? html` - ${titleTemplate} -
- ${this.hass.localize( - `ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}` - )}${dashboard.filename - ? html` – ${dashboard.filename} ` - : ""} -
- ` - : titleTemplate; - }, + template: narrow + ? undefined + : (dashboard) => html` + ${dashboard.title} + ${dashboard.default + ? html` + + + ${this.hass.localize( + `ui.panel.config.lovelace.dashboards.default_dashboard` + )} + + ` + : ""} + `, }, }; - if (!narrow) { - columns.mode = { + columns.mode = { + title: localize( + "ui.panel.config.lovelace.dashboards.picker.headers.conf_mode" + ), + sortable: true, + filterable: true, + width: "20%", + template: (dashboard) => html` + ${this.hass.localize( + `ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}` + ) || dashboard.mode} + `, + }; + if (dashboards.some((dashboard) => dashboard.filename)) { + columns.filename = { title: localize( - "ui.panel.config.lovelace.dashboards.picker.headers.conf_mode" + "ui.panel.config.lovelace.dashboards.picker.headers.filename" ), + width: "15%", sortable: true, filterable: true, - width: "20%", - template: (dashboard) => html` - ${this.hass.localize( - `ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}` - ) || dashboard.mode} - `, - }; - if (dashboards.some((dashboard) => dashboard.filename)) { - columns.filename = { - title: localize( - "ui.panel.config.lovelace.dashboards.picker.headers.filename" - ), - width: "15%", - sortable: true, - filterable: true, - }; - } - columns.require_admin = { - title: localize( - "ui.panel.config.lovelace.dashboards.picker.headers.require_admin" - ), - sortable: true, - type: "icon", - width: "100px", - template: (dashboard) => - dashboard.require_admin - ? html`` - : html`—`, - }; - columns.show_in_sidebar = { - title: localize( - "ui.panel.config.lovelace.dashboards.picker.headers.sidebar" - ), - type: "icon", - width: "121px", - template: (dashboard) => - dashboard.show_in_sidebar - ? html`` - : html`—`, }; } + columns.require_admin = { + title: localize( + "ui.panel.config.lovelace.dashboards.picker.headers.require_admin" + ), + sortable: true, + type: "icon", + hidden: narrow, + width: "100px", + template: (dashboard) => + dashboard.require_admin + ? html`` + : html`—`, + }; + columns.show_in_sidebar = { + title: localize( + "ui.panel.config.lovelace.dashboards.picker.headers.sidebar" + ), + type: "icon", + hidden: narrow, + width: "121px", + template: (dashboard) => + dashboard.show_in_sidebar + ? html`` + : html`—`, + }; columns.url_path = { title: "", @@ -216,6 +220,7 @@ export class HaConfigLovelaceDashboards extends LitElement { "ui.panel.config.lovelace.dashboards.picker.headers.url" ), filterable: true, + showNarrow: true, width: "100px", template: (dashboard) => narrow @@ -311,6 +316,9 @@ export class HaConfigLovelaceDashboards extends LitElement { )} .data=${this._getItems(this._dashboards)} .initialSorting=${this._activeSorting} + .columnOrder=${this._activeColumnOrder} + .hiddenColumns=${this._activeHiddenColumns} + @columns-changed=${this._handleColumnsChanged} @sorting-changed=${this._handleSortingChanged} .filter=${this._filter} @search-changed=${this._handleSearchChange} @@ -467,6 +475,11 @@ export class HaConfigLovelaceDashboards extends LitElement { private _handleSearchChange(ev: CustomEvent) { this._filter = ev.detail.value; } + + private _handleColumnsChanged(ev: CustomEvent) { + this._activeColumnOrder = ev.detail.columnOrder; + this._activeHiddenColumns = ev.detail.hiddenColumns; + } } declare global { diff --git a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts index ab87058e69..2c91a30a88 100644 --- a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts +++ b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts @@ -67,12 +67,27 @@ export class HaConfigLovelaceRescources extends LitElement { }) private _activeSorting?: SortingChangedEvent; + @storage({ + key: "lovelace-resources-table-column-order", + state: false, + subscribe: false, + }) + private _activeColumnOrder?: string[]; + + @storage({ + key: "lovelace-resources-table-hidden-columns", + state: false, + subscribe: false, + }) + private _activeHiddenColumns?: string[]; + private _columns = memoize( ( _language, localize: LocalizeFunc ): DataTableColumnContainer => ({ url: { + main: true, title: localize( "ui.panel.config.lovelace.resources.picker.headers.url" ), @@ -145,6 +160,9 @@ export class HaConfigLovelaceRescources extends LitElement { "ui.panel.config.lovelace.resources.picker.no_resources" )} .initialSorting=${this._activeSorting} + .columnOrder=${this._activeColumnOrder} + .hiddenColumns=${this._activeHiddenColumns} + @columns-changed=${this._handleColumnsChanged} @sorting-changed=${this._handleSortingChanged} .filter=${this._filter} @search-changed=${this._handleSearchChange} @@ -266,6 +284,11 @@ export class HaConfigLovelaceRescources extends LitElement { this._filter = ev.detail.value; } + private _handleColumnsChanged(ev: CustomEvent) { + this._activeColumnOrder = ev.detail.columnOrder; + this._activeHiddenColumns = ev.detail.hiddenColumns; + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index 4f2fca1c96..0f47b0644c 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -180,6 +180,20 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { }) private _activeCollapsed?: string; + @storage({ + key: "scene-table-column-order", + state: false, + subscribe: false, + }) + private _activeColumnOrder?: string[]; + + @storage({ + key: "scene-table-hidden-columns", + state: false, + subscribe: false, + }) + private _activeHiddenColumns?: string[]; + private _sizeController = new ResizeController(this, { callback: (entries) => entries[0]?.contentRect.width, }); @@ -225,11 +239,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { ); private _columns = memoizeOne( - (narrow, localize: LocalizeFunc): DataTableColumnContainer => { + (localize: LocalizeFunc): DataTableColumnContainer => { const columns: DataTableColumnContainer = { icon: { title: "", label: localize("ui.panel.config.scene.picker.headers.state"), + moveable: false, + showNarrow: true, type: "icon", template: (scene) => html` html` -
${scene.name}
- ${scene.labels.length + extraTemplate: (scene) => + scene.labels.length ? html`` - : nothing} - `, + : nothing, }, area: { title: localize("ui.panel.config.scene.picker.headers.area"), @@ -281,7 +295,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { ), sortable: true, width: "30%", - hidden: narrow, template: (scene) => { const lastActivated = scene.state; if (!lastActivated || isUnavailableState(lastActivated)) { @@ -300,6 +313,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { only_editable: { title: "", width: "56px", + showNarrow: true, template: (scene) => !scene.attributes.id ? html` @@ -319,6 +333,9 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { title: "", width: "64px", type: "overflow-menu", + showNarrow: true, + moveable: false, + hideable: false, template: (scene) => html` entries[0]?.contentRect.width, }); @@ -232,14 +246,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { ); private _columns = memoizeOne( - ( - narrow, - localize: LocalizeFunc, - locale: HomeAssistant["locale"] - ): DataTableColumnContainer => { + (localize: LocalizeFunc): DataTableColumnContainer => { const columns: DataTableColumnContainer = { icon: { title: "", + showNarrow: true, + moveable: false, label: localize("ui.panel.config.script.picker.headers.state"), type: "icon", template: (script) => @@ -259,30 +271,13 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { filterable: true, direction: "asc", grows: true, - template: (script) => { - const date = new Date(script.last_triggered); - const now = new Date(); - const dayDifference = differenceInDays(now, date); - return html` -
${script.name}
- ${narrow - ? html`
- ${this.hass.localize("ui.card.automation.last_triggered")}: - ${script.attributes.last_triggered - ? dayDifference > 3 - ? formatShortDateTime(date, locale, this.hass.config) - : relativeTime(date, locale) - : localize("ui.components.relative_time.never")} -
` - : nothing} - ${script.labels.length - ? html`` - : nothing} - `; - }, + extraTemplate: (script) => + script.labels.length + ? html`` + : nothing, }, area: { title: localize("ui.panel.config.script.picker.headers.area"), @@ -305,7 +300,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { template: (script) => script.labels.map((lbl) => lbl.name).join(" "), }, last_triggered: { - hidden: narrow, sortable: true, width: "40%", title: localize("ui.card.automation.last_triggered"), @@ -330,6 +324,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { title: "", width: "64px", type: "overflow-menu", + showNarrow: true, + moveable: false, + hideable: false, template: (script) => html` { - const columns: DataTableColumnContainer = { - icon: { - title: "", - label: localize("ui.panel.config.tag.headers.icon"), - type: "icon", - template: (tag) => html``, - }, - display_name: { - title: localize("ui.panel.config.tag.headers.name"), - main: true, - sortable: true, - filterable: true, - grows: true, - template: (tag) => - html`${tag.display_name} - ${narrow - ? html`
- ${tag.last_scanned_datetime - ? html`` - : this.hass.localize("ui.panel.config.tag.never_scanned")} -
` - : ""}`, - }, - last_scanned_datetime: { - title: localize("ui.panel.config.tag.headers.last_scanned"), - sortable: true, - hidden: narrow, - direction: "desc", - width: "20%", - template: (tag) => html` - ${tag.last_scanned_datetime - ? html`` - : this.hass.localize("ui.panel.config.tag.never_scanned")} - `, - }, - }; - if (this._canWriteTags) { - columns.write = { - title: "", - label: localize("ui.panel.config.tag.headers.write"), - type: "icon-button", - template: (tag) => - html` `, - }; - } - columns.automation = { + private _columns = memoizeOne((localize: LocalizeFunc) => { + const columns: DataTableColumnContainer = { + icon: { title: "", + moveable: false, + showNarrow: true, + label: localize("ui.panel.config.tag.headers.icon"), + type: "icon", + template: (tag) => html``, + }, + display_name: { + title: localize("ui.panel.config.tag.headers.name"), + main: true, + sortable: true, + filterable: true, + grows: true, + }, + last_scanned_datetime: { + title: localize("ui.panel.config.tag.headers.last_scanned"), + sortable: true, + direction: "desc", + width: "20%", + template: (tag) => html` + ${tag.last_scanned_datetime + ? html`` + : this.hass.localize("ui.panel.config.tag.never_scanned")} + `, + }, + }; + if (this._canWriteTags) { + columns.write = { + title: "", + label: localize("ui.panel.config.tag.headers.write"), type: "icon-button", + showNarrow: true, template: (tag) => - html` `, }; - columns.edit = { - title: "", - type: "icon-button", - template: (tag) => - html` `, - }; - return columns; } - ); + columns.automation = { + title: "", + type: "icon-button", + showNarrow: true, + template: (tag) => + html``, + }; + columns.edit = { + title: "", + type: "icon-button", + showNarrow: true, + hideable: false, + moveable: false, + template: (tag) => + html``, + }; + return columns; + }); private _data = memoizeOne((tags: Tag[]): TagRowData[] => tags.map((tag) => ({ @@ -191,11 +180,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) { back-path="/config" .route=${this.route} .tabs=${configSections.tags} - .columns=${this._columns( - this.narrow, - this.hass.language, - this.hass.localize - )} + .columns=${this._columns(this.hass.localize)} .data=${this._data(this._tags)} .noDataText=${this.hass.localize("ui.panel.config.tag.no_tags")} .filter=${this._filter} diff --git a/src/panels/config/users/ha-config-users.ts b/src/panels/config/users/ha-config-users.ts index 89bdea4bf7..553d3f1a02 100644 --- a/src/panels/config/users/ha-config-users.ts +++ b/src/panels/config/users/ha-config-users.ts @@ -46,6 +46,20 @@ export class HaConfigUsers extends LitElement { @storage({ key: "users-table-grouping", state: false, subscribe: false }) private _activeGrouping?: string; + @storage({ + key: "users-table-column-order", + state: false, + subscribe: false, + }) + private _activeColumnOrder?: string[]; + + @storage({ + key: "users-table-hidden-columns", + state: false, + subscribe: false, + }) + private _activeHiddenColumns?: string[]; + @storage({ storage: "sessionStorage", key: "users-table-search", @@ -72,17 +86,6 @@ export class HaConfigUsers extends LitElement { width: "25%", direction: "asc", grows: true, - template: (user) => - narrow - ? html` ${user.name}
-
- ${user.username ? `${user.username} |` : ""} - ${localize(`groups.${user.group_ids[0]}`)} -
` - : html` ${user.name || - this.hass!.localize( - "ui.panel.config.users.editor.unnamed_user" - )}`, }, username: { title: localize("ui.panel.config.users.picker.headers.username"), @@ -90,7 +93,6 @@ export class HaConfigUsers extends LitElement { filterable: true, width: "20%", direction: "asc", - hidden: narrow, template: (user) => html`${user.username || "—"}`, }, group: { @@ -100,7 +102,6 @@ export class HaConfigUsers extends LitElement { groupable: true, width: "20%", direction: "asc", - hidden: narrow, }, is_active: { title: this.hass.localize( @@ -154,6 +155,7 @@ export class HaConfigUsers extends LitElement { filterable: false, width: "104px", hidden: !narrow, + showNarrow: true, template: (user) => { const badges = computeUserBadges(this.hass, user, false); return html`${badges.map( @@ -186,6 +188,9 @@ export class HaConfigUsers extends LitElement { .tabs=${configSections.persons} .columns=${this._columns(this.narrow, this.hass.localize)} .data=${this._userData(this._users, this.hass.localize)} + .columnOrder=${this._activeColumnOrder} + .hiddenColumns=${this._activeHiddenColumns} + @columns-changed=${this._handleColumnsChanged} .initialGroupColumn=${this._activeGrouping} .initialCollapsedGroups=${this._activeCollapsed} .initialSorting=${this._activeSorting} @@ -213,6 +218,7 @@ export class HaConfigUsers extends LitElement { private _userData = memoizeOne((users: User[], localize: LocalizeFunc) => users.map((user) => ({ ...user, + name: user.name || localize("ui.panel.config.users.editor.unnamed_user"), group: localize(`groups.${user.group_ids[0]}`), })) ); @@ -302,6 +308,11 @@ export class HaConfigUsers extends LitElement { private _handleSearchChange(ev: CustomEvent) { this._filter = ev.detail.value; } + + private _handleColumnsChanged(ev: CustomEvent) { + this._activeColumnOrder = ev.detail.columnOrder; + this._activeHiddenColumns = ev.detail.hiddenColumns; + } } declare global { diff --git a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts index ff901348e6..7a2b5e9c49 100644 --- a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts +++ b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts @@ -118,6 +118,20 @@ export class VoiceAssistantsExpose extends LitElement { }) private _activeCollapsed?: string; + @storage({ + key: "voice-expose-table-column-order", + state: false, + subscribe: false, + }) + private _activeColumnOrder?: string[]; + + @storage({ + key: "voice-expose-table-hidden-columns", + state: false, + subscribe: false, + }) + private _activeHiddenColumns?: string[]; + @query("hass-tabs-subpage-data-table", true) private _dataTable!: HaTabsSubpageDataTable; @@ -137,6 +151,7 @@ export class VoiceAssistantsExpose extends LitElement { icon: { title: "", type: "icon", + moveable: false, hidden: narrow, template: (entry) => html` html` - ${entry.name}
-
${entry.entity_id}
- `, + template: narrow + ? undefined + : (entry) => html` + ${entry.name}
+
${entry.entity_id}
+ `, + }, + // For search & narrow + entity_id: { + title: localize( + "ui.panel.config.voice_assistants.expose.headers.entity_id" + ), + hidden: !narrow, + filterable: true, }, domain: { title: localize( @@ -171,7 +196,6 @@ export class VoiceAssistantsExpose extends LitElement { title: localize("ui.panel.config.voice_assistants.expose.headers.area"), sortable: true, groupable: true, - hidden: narrow, filterable: true, width: "15%", }, @@ -179,6 +203,7 @@ export class VoiceAssistantsExpose extends LitElement { title: localize( "ui.panel.config.voice_assistants.expose.headers.assistants" ), + showNarrow: true, sortable: true, filterable: true, width: "160px", @@ -208,7 +233,6 @@ export class VoiceAssistantsExpose extends LitElement { ), sortable: true, filterable: true, - hidden: narrow, width: "15%", template: (entry) => entry.aliases.length === 0 @@ -230,12 +254,6 @@ export class VoiceAssistantsExpose extends LitElement { .path=${mdiCloseCircleOutline} >
`, }, - // For search - entity_id: { - title: "", - hidden: true, - filterable: true, - }, }) ); @@ -552,6 +570,9 @@ export class VoiceAssistantsExpose extends LitElement { .initialSorting=${this._activeSorting} .initialGroupColumn=${this._activeGrouping} .initialCollapsedGroups=${this._activeCollapsed} + .columnOrder=${this._activeColumnOrder} + .hiddenColumns=${this._activeHiddenColumns} + @columns-changed=${this._handleColumnsChanged} @sorting-changed=${this._handleSortingChanged} @selection-changed=${this._handleSelectionChanged} @grouping-changed=${this._handleGroupingChanged} @@ -757,6 +778,11 @@ export class VoiceAssistantsExpose extends LitElement { this._activeCollapsed = ev.detail.value; } + private _handleColumnsChanged(ev: CustomEvent) { + this._activeColumnOrder = ev.detail.columnOrder; + this._activeHiddenColumns = ev.detail.hiddenColumns; + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/translations/en.json b/src/translations/en.json index f813a386f1..822152b221 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -530,7 +530,8 @@ "selected": "Selected {selected}", "close_select_mode": "Close selection mode", "select_all": "Select all", - "select_none": "Select none" + "select_none": "Select none", + "settings": "Customize table" }, "config-entry-picker": { "config_entry": "Integration" @@ -799,7 +800,14 @@ "filtering_by": "Filtering by", "hidden": "{number} hidden", "clear": "Clear", - "ungrouped": "Ungrouped" + "ungrouped": "Ungrouped", + "settings": { + "header": "Customize", + "hide": "Hide column {title}", + "show": "Show column {title}", + "done": "Done", + "restore": "Restore defaults" + } }, "media-browser": { "tts": { @@ -2071,6 +2079,7 @@ "download_backup": "[%key:supervisor::backup::download_backup%]", "remove_backup": "[%key:supervisor::backup::delete_backup_title%]", "name": "[%key:supervisor::backup::name%]", + "path": "Path", "size": "[%key:supervisor::backup::size%]", "created": "[%key:supervisor::backup::created%]", "no_backups": "[%key:supervisor::backup::no_backups%]", @@ -2665,6 +2674,7 @@ "caption": "Expose", "headers": { "name": "Name", + "entity_id": "Entity ID", "area": "Area", "domain": "Domain", "assistants": "Assistants", @@ -4040,6 +4050,7 @@ "update_device_error": "Updating the device failed", "disabled": "Disabled", "data_table": { + "icon": "Icon", "device": "Device", "manufacturer": "Manufacturer", "model": "Model",