mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Allow to hide and sort columns in data tables (#21168)
* Allow to hide and sort columns in data tables * fix unused * store
This commit is contained in:
parent
adea384f40
commit
7d28f3f585
280
src/components/data-table/dialog-data-table-settings.ts
Normal file
280
src/components/data-table/dialog-data-table-settings.ts
Normal file
@ -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`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.components.data-table.settings.header")
|
||||
)}
|
||||
>
|
||||
<ha-sortable
|
||||
@item-moved=${this._columnMoved}
|
||||
draggable-selector=".draggable"
|
||||
handle-selector=".handle"
|
||||
>
|
||||
<mwc-list>
|
||||
${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`<ha-list-item
|
||||
hasMeta
|
||||
class=${classMap({
|
||||
hidden: !isVisible,
|
||||
draggable: canMove && isVisible,
|
||||
})}
|
||||
graphic="icon"
|
||||
noninteractive
|
||||
>${col.title || col.label || col.key}
|
||||
${canMove && isVisible
|
||||
? html`<ha-svg-icon
|
||||
class="handle"
|
||||
.path=${mdiDrag}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
tabindex="0"
|
||||
class="action"
|
||||
.disabled=${!canHide}
|
||||
.hidden=${!isVisible}
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="meta"
|
||||
.label=${this.hass!.localize(
|
||||
`ui.components.data-table.settings.${isVisible ? "hide" : "show"}`,
|
||||
{ title: typeof col.title === "string" ? col.title : "" }
|
||||
)}
|
||||
.column=${col.key}
|
||||
@click=${this._toggle}
|
||||
></ha-icon-button>
|
||||
</ha-list-item>`;
|
||||
}
|
||||
)}
|
||||
</mwc-list>
|
||||
</ha-sortable>
|
||||
<ha-button slot="secondaryAction" @click=${this._reset}
|
||||
>${this.hass.localize(
|
||||
"ui.components.data-table.settings.restore"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.components.data-table.settings.done")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -65,6 +65,10 @@ export interface DataTableSortColumnData {
|
||||
valueColumn?: string;
|
||||
direction?: SortingDirection;
|
||||
groupable?: boolean;
|
||||
moveable?: boolean;
|
||||
hideable?: boolean;
|
||||
defaultHidden?: boolean;
|
||||
showNarrow?: boolean;
|
||||
}
|
||||
|
||||
export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
||||
@ -79,6 +83,7 @@ export interface DataTableColumnData<T = any> 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`
|
||||
<div class="mdc-data-table">
|
||||
<slot name="header" @slotchange=${this._calcTableHeight}>
|
||||
@ -326,9 +370,14 @@ export class HaDataTable extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${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}
|
||||
></lit-virtualizer>
|
||||
`}
|
||||
</div>
|
||||
@ -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 {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${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`<div class="primary">${row[key]}</div>
|
||||
<div class="secondary">
|
||||
${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]}`
|
||||
)}
|
||||
</div>
|
||||
${column.extraTemplate
|
||||
? column.extraTemplate(row)
|
||||
: nothing}`
|
||||
: html`${row[key]}${column.extraTemplate
|
||||
? column.extraTemplate(row)
|
||||
: nothing}`}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
@ -861,6 +952,7 @@ export class HaDataTable extends LitElement {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell {
|
||||
|
26
src/components/data-table/show-dialog-data-table-settings.ts
Normal file
26
src/components/data-table/show-dialog-data-table-settings.ts
Normal file
@ -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,
|
||||
});
|
||||
};
|
@ -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`<ha-assist-chip
|
||||
class="has-dropdown select-mode-chip"
|
||||
@click=${this._openSettings}
|
||||
.title=${localize("ui.components.subpage-data-table.settings")}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
|
||||
</ha-assist-chip>`;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
@ -416,6 +430,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
: ""}
|
||||
<ha-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.columns=${this.columns}
|
||||
.data=${this.data}
|
||||
.noDataText=${this.noDataText}
|
||||
@ -430,6 +445,8 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
.groupColumn=${this._groupColumn}
|
||||
.groupOrder=${this.groupOrder}
|
||||
.initialCollapsedGroups=${this.initialCollapsedGroups}
|
||||
.columnOrder=${this.columnOrder}
|
||||
.hiddenColumns=${this.hiddenColumns}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`
|
||||
@ -438,7 +455,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
<div class="table-header">
|
||||
${this.hasFilters && !this.showFilters
|
||||
? html`${filterButton}`
|
||||
: nothing}${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}
|
||||
: nothing}${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}${settingsButton}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
@ -448,7 +465,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
${this.hasFilters && !this.showFilters
|
||||
? html`${filterButton}`
|
||||
: nothing}
|
||||
${selectModeBtn}${groupByMenu}${sortByMenu}
|
||||
${selectModeBtn}${groupByMenu}${sortByMenu}${settingsButton}
|
||||
</div>`}
|
||||
</ha-data-table>`}
|
||||
<div slot="fab"><slot name="fab"></slot></div>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
@ -272,30 +288,13 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (automation) => {
|
||||
const date = new Date(automation.attributes.last_triggered);
|
||||
const now = new Date();
|
||||
const dayDifference = differenceInDays(now, date);
|
||||
return html`
|
||||
<div style="font-size: 14px;">${automation.name}</div>
|
||||
${narrow
|
||||
? html`<div class="secondary">
|
||||
${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")}
|
||||
</div>`
|
||||
: nothing}
|
||||
${automation.labels.length
|
||||
? html`<ha-data-table-labels
|
||||
@label-clicked=${this._labelClicked}
|
||||
.labels=${automation.labels}
|
||||
></ha-data-table-labels>`
|
||||
: nothing}
|
||||
`;
|
||||
},
|
||||
extraTemplate: (automation) =>
|
||||
automation.labels.length
|
||||
? html`<ha-data-table-labels
|
||||
@label-clicked=${this._labelClicked}
|
||||
.labels=${automation.labels}
|
||||
></ha-data-table-labels>`
|
||||
: 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`
|
||||
<ha-entity-toggle
|
||||
@ -356,6 +354,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
title: "",
|
||||
width: "64px",
|
||||
type: "icon-button",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
template: (automation) => html`
|
||||
<ha-icon-button
|
||||
.automation=${automation}
|
||||
@ -545,6 +546,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.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}
|
||||
@ -1415,6 +1419,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,
|
||||
|
@ -60,14 +60,19 @@ class HaConfigBackup extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: (backup) =>
|
||||
html`${backup.name}
|
||||
<div class="secondary">${backup.path}</div>`,
|
||||
template: narrow
|
||||
? undefined
|
||||
: (backup) =>
|
||||
html`${backup.name}
|
||||
<div class="secondary">${backup.path}</div>`,
|
||||
},
|
||||
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`<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
|
@ -107,6 +107,20 @@ class HaBlueprintOverview extends LitElement {
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@storage({
|
||||
key: "blueprint-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "blueprint-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "blueprint-table-search",
|
||||
@ -154,8 +168,6 @@ class HaBlueprintOverview extends LitElement {
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(
|
||||
narrow,
|
||||
_language,
|
||||
localize: LocalizeFunc
|
||||
): DataTableColumnContainer<BlueprintMetaDataPath> => ({
|
||||
name: {
|
||||
@ -165,19 +177,12 @@ class HaBlueprintOverview extends LitElement {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: narrow
|
||||
? (blueprint) => html`
|
||||
${blueprint.name}<br />
|
||||
<div class="secondary">${blueprint.path}</div>
|
||||
`
|
||||
: 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`<ha-svg-icon
|
||||
@ -280,11 +287,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.language,
|
||||
this.hass.localize
|
||||
)}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._processedBlueprints(this.blueprints, this.hass.localize)}
|
||||
id="fullpath"
|
||||
.noDataText=${this.hass.localize(
|
||||
@ -313,6 +316,9 @@ class HaBlueprintOverview extends 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}
|
||||
@ -556,6 +562,11 @@ class HaBlueprintOverview 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;
|
||||
}
|
||||
|
@ -154,6 +154,20 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
@storage({ key: "devices-table-collapsed", state: false, subscribe: false })
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@storage({
|
||||
key: "devices-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "devices-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
@ -434,10 +448,13 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
typeof this._devicesAndFilterDomains
|
||||
>["devicesOutput"][number];
|
||||
|
||||
const columns: DataTableColumnContainer<DeviceItem> = {
|
||||
return {
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.devices.data_table.icon"),
|
||||
type: "icon",
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
template: (device) =>
|
||||
device.domains.length
|
||||
? html`<img
|
||||
@ -452,19 +469,14 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
/>`
|
||||
: "",
|
||||
},
|
||||
};
|
||||
|
||||
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`
|
||||
<div style="font-size: 14px;">${device.name}</div>
|
||||
<div class="secondary">${device.area} | ${device.integration}</div>
|
||||
extraTemplate: (device) => html`
|
||||
${device.label_entries.length
|
||||
? html`
|
||||
<ha-data-table-labels
|
||||
@ -473,112 +485,89 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
`
|
||||
: nothing}
|
||||
`,
|
||||
};
|
||||
} else {
|
||||
columns.name = {
|
||||
title: localize("ui.panel.config.devices.data_table.device"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (device) => html`
|
||||
<div style="font-size: 14px;">${device.name}</div>
|
||||
${device.label_entries.length
|
||||
? html`
|
||||
<ha-data-table-labels
|
||||
.labels=${device.label_entries}
|
||||
></ha-data-table-labels>
|
||||
`
|
||||
: 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}
|
||||
<ha-battery-icon
|
||||
.hass=${this.hass}
|
||||
.batteryStateObj=${battery}
|
||||
.batteryChargingStateObj=${batteryCharging}
|
||||
></ha-battery-icon>
|
||||
`
|
||||
: 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}
|
||||
<ha-battery-icon
|
||||
.hass=${this.hass}
|
||||
.batteryStateObj=${battery}
|
||||
.batteryChargingStateObj=${batteryCharging}
|
||||
></ha-battery-icon>
|
||||
`
|
||||
: 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<DeviceItem>;
|
||||
});
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
@ -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`
|
||||
|
@ -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<EntityRow> => ({
|
||||
(localize: LocalizeFunc): DataTableColumnContainer<EntityRow> => ({
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.entities.picker.headers.state_icon"),
|
||||
type: "icon",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
template: (entry) =>
|
||||
entry.icon
|
||||
? html`<ha-icon .icon=${entry.icon}></ha-icon>`
|
||||
@ -283,32 +295,23 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (entry) => html`
|
||||
<div style="font-size: 14px;">${entry.name}</div>
|
||||
${narrow
|
||||
? html`<div class="secondary">
|
||||
${entry.entity_id} | ${entry.localized_platform}
|
||||
</div>`
|
||||
: nothing}
|
||||
${entry.label_entries.length
|
||||
extraTemplate: (entry) =>
|
||||
entry.label_entries.length
|
||||
? html`
|
||||
<ha-data-table-labels
|
||||
.labels=${entry.label_entries}
|
||||
></ha-data-table-labels>
|
||||
`
|
||||
: 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,
|
||||
|
@ -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<string, EntityRegistryEntry>;
|
||||
@ -243,14 +257,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(
|
||||
narrow: boolean,
|
||||
localize: LocalizeFunc
|
||||
): DataTableColumnContainer<HelperItem> => ({
|
||||
(localize: LocalizeFunc): DataTableColumnContainer<HelperItem> => ({
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.helpers.picker.headers.icon"),
|
||||
type: "icon",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
template: (helper) =>
|
||||
helper.entity
|
||||
? html`<ha-state-icon
|
||||
@ -269,23 +282,17 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
template: (helper) => html`
|
||||
<div style="font-size: 14px;">${helper.name}</div>
|
||||
${narrow
|
||||
? html`<div class="secondary">${helper.entity_id}</div> `
|
||||
: nothing}
|
||||
${helper.label_entries.length
|
||||
extraTemplate: (helper) =>
|
||||
helper.label_entries.length
|
||||
? html`
|
||||
<ha-data-table-labels
|
||||
.labels=${helper.label_entries}
|
||||
></ha-data-table-labels>
|
||||
`
|
||||
: 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`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
@ -556,11 +566,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${helpers}
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.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}
|
||||
@ -1084,6 +1097,11 @@ ${rejected
|
||||
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,
|
||||
|
@ -66,10 +66,26 @@ export class HaConfigLabels extends LitElement {
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({
|
||||
key: "labels-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "labels-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
private _columns = memoizeOne((localize: LocalizeFunc) => {
|
||||
const columns: DataTableColumnContainer<LabelRegistryEntry> = {
|
||||
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 {
|
||||
|
@ -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<DataTableItem> = {
|
||||
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`
|
||||
<ha-svg-icon
|
||||
style="padding-left: 10px; padding-inline-start: 10px; direction: var(--direction);"
|
||||
.path=${mdiCheckCircleOutline}
|
||||
></ha-svg-icon>
|
||||
<simple-tooltip animation-delay="0">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.default_dashboard`
|
||||
)}
|
||||
</simple-tooltip>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
return narrow
|
||||
? html`
|
||||
${titleTemplate}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}`
|
||||
)}${dashboard.filename
|
||||
? html` – ${dashboard.filename} `
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: titleTemplate;
|
||||
},
|
||||
template: narrow
|
||||
? undefined
|
||||
: (dashboard) => html`
|
||||
${dashboard.title}
|
||||
${dashboard.default
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
style="padding-left: 10px; padding-inline-start: 10px; direction: var(--direction);"
|
||||
.path=${mdiCheckCircleOutline}
|
||||
></ha-svg-icon>
|
||||
<simple-tooltip animation-delay="0">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.default_dashboard`
|
||||
)}
|
||||
</simple-tooltip>
|
||||
`
|
||||
: ""}
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
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`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: 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`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: 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`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: 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`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: 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 {
|
||||
|
@ -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<LovelaceResource> => ({
|
||||
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,
|
||||
|
@ -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<SceneItem> = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.scene.picker.headers.state"),
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
type: "icon",
|
||||
template: (scene) => html`
|
||||
<ha-state-icon
|
||||
@ -245,15 +261,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (scene) => html`
|
||||
<div style="font-size: 14px;">${scene.name}</div>
|
||||
${scene.labels.length
|
||||
extraTemplate: (scene) =>
|
||||
scene.labels.length
|
||||
? html`<ha-data-table-labels
|
||||
@label-clicked=${this._labelClicked}
|
||||
.labels=${scene.labels}
|
||||
></ha-data-table-labels>`
|
||||
: 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`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
@ -536,11 +553,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
id="entity_id"
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.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}
|
||||
@ -1155,6 +1175,11 @@ ${rejected
|
||||
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,
|
||||
|
@ -184,6 +184,20 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@storage({
|
||||
key: "script-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "script-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
@ -232,14 +246,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(
|
||||
narrow,
|
||||
localize: LocalizeFunc,
|
||||
locale: HomeAssistant["locale"]
|
||||
): DataTableColumnContainer<ScriptItem> => {
|
||||
(localize: LocalizeFunc): DataTableColumnContainer<ScriptItem> => {
|
||||
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`
|
||||
<div style="font-size: 14px;">${script.name}</div>
|
||||
${narrow
|
||||
? html`<div class="secondary">
|
||||
${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")}
|
||||
</div>`
|
||||
: nothing}
|
||||
${script.labels.length
|
||||
? html`<ha-data-table-labels
|
||||
@label-clicked=${this._labelClicked}
|
||||
.labels=${script.labels}
|
||||
></ha-data-table-labels>`
|
||||
: nothing}
|
||||
`;
|
||||
},
|
||||
extraTemplate: (script) =>
|
||||
script.labels.length
|
||||
? html`<ha-data-table-labels
|
||||
@label-clicked=${this._labelClicked}
|
||||
.labels=${script.labels}
|
||||
></ha-data-table-labels>`
|
||||
: 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`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
@ -539,6 +536,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.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}
|
||||
@ -553,11 +553,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.localize,
|
||||
this.hass.locale
|
||||
)}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${scripts}
|
||||
.empty=${!this.scripts.length}
|
||||
.activeFilters=${this._activeFilters}
|
||||
@ -1270,6 +1266,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,
|
||||
|
@ -66,93 +66,82 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean, _language, localize: LocalizeFunc) => {
|
||||
const columns: DataTableColumnContainer<TagRowData> = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.tag.headers.icon"),
|
||||
type: "icon",
|
||||
template: (tag) => html`<tag-image .tag=${tag}></tag-image>`,
|
||||
},
|
||||
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`<div class="secondary">
|
||||
${tag.last_scanned_datetime
|
||||
? html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${tag.last_scanned_datetime}
|
||||
capitalize
|
||||
></ha-relative-time>`
|
||||
: this.hass.localize("ui.panel.config.tag.never_scanned")}
|
||||
</div>`
|
||||
: ""}`,
|
||||
},
|
||||
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`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${tag.last_scanned_datetime}
|
||||
capitalize
|
||||
></ha-relative-time>`
|
||||
: 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` <ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleWriteClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.write")}
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
}
|
||||
columns.automation = {
|
||||
private _columns = memoizeOne((localize: LocalizeFunc) => {
|
||||
const columns: DataTableColumnContainer<TagRowData> = {
|
||||
icon: {
|
||||
title: "",
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
label: localize("ui.panel.config.tag.headers.icon"),
|
||||
type: "icon",
|
||||
template: (tag) => html`<tag-image .tag=${tag}></tag-image>`,
|
||||
},
|
||||
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`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${tag.last_scanned_datetime}
|
||||
capitalize
|
||||
></ha-relative-time>`
|
||||
: 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` <ha-icon-button
|
||||
html`<ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleAutomationClick}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.tag.create_automation"
|
||||
)}
|
||||
.path=${mdiRobot}
|
||||
@click=${this._handleWriteClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.write")}
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
columns.edit = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (tag) =>
|
||||
html` <ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleEditClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.edit")}
|
||||
.path=${mdiCog}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
columns.automation = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
showNarrow: true,
|
||||
template: (tag) =>
|
||||
html`<ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleAutomationClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.create_automation")}
|
||||
.path=${mdiRobot}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
columns.edit = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
showNarrow: true,
|
||||
hideable: false,
|
||||
moveable: false,
|
||||
template: (tag) =>
|
||||
html`<ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleEditClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.edit")}
|
||||
.path=${mdiCog}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
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}
|
||||
|
@ -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}<br />
|
||||
<div class="secondary">
|
||||
${user.username ? `${user.username} |` : ""}
|
||||
${localize(`groups.${user.group_ids[0]}`)}
|
||||
</div>`
|
||||
: 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 {
|
||||
|
@ -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`
|
||||
<ha-state-icon
|
||||
@ -153,10 +168,20 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (entry) => html`
|
||||
${entry.name}<br />
|
||||
<div class="secondary">${entry.entity_id}</div>
|
||||
`,
|
||||
template: narrow
|
||||
? undefined
|
||||
: (entry) => html`
|
||||
${entry.name}<br />
|
||||
<div class="secondary">${entry.entity_id}</div>
|
||||
`,
|
||||
},
|
||||
// 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}
|
||||
></ha-icon-button>`,
|
||||
},
|
||||
// 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,
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user