import "@home-assistant/webawesome/dist/components/divider/divider"; import { ResizeController } from "@lit-labs/observers/resize-controller"; import { mdiArrowDown, mdiArrowUp, mdiClose, mdiFilterVariant, mdiFilterVariantRemove, mdiFormatListChecks, mdiMenuDown, mdiTableCog, mdiUnfoldLessHorizontal, mdiUnfoldMoreHorizontal, } from "@mdi/js"; import type { TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../common/dom/fire_event"; import type { LocalizeFunc } from "../common/translations/localize"; import "../components/chips/ha-assist-chip"; import "../components/data-table/ha-data-table"; import type { DataTableColumnContainer, DataTableRowData, HaDataTable, SortingDirection, } from "../components/data-table/ha-data-table"; import { showDataTableSettingsDialog } from "../components/data-table/show-dialog-data-table-settings"; import "../components/ha-button"; import "../components/ha-dialog"; import "../components/ha-dialog-header"; import "../components/ha-dropdown"; import "../components/ha-icon-button"; import "../components/ha-svg-icon"; import type { HaDropdownSelectEvent } from "../components/ha-dropdown"; import "../components/ha-dropdown-item"; import "../components/search-input-outlined"; import { KeyboardShortcutMixin } from "../mixins/keyboard-shortcut-mixin"; import type { HomeAssistant, Route } from "../types"; import "./hass-tabs-subpage"; import type { PageNavigation } from "./hass-tabs-subpage"; @customElement("hass-tabs-subpage-data-table") export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public localizeFunc?: LocalizeFunc; @property({ attribute: "is-wide", type: Boolean }) public isWide = false; @property({ type: Boolean, reflect: true }) public narrow = false; @property({ type: Boolean }) public supervisor = false; @property({ type: Boolean, attribute: "main-page" }) public mainPage = false; @property({ attribute: false }) public initialCollapsedGroups: string[] = []; /** * Object with the columns. * @type {Object} */ @property({ type: Object }) public columns: DataTableColumnContainer = {}; /** * Data to show in the table. * @type {Array} */ @property({ type: Array }) public data: DataTableRowData[] = []; /** * Should rows be selectable. * @type {Boolean} */ @property({ type: Boolean }) public selectable = false; /** * Should rows be clickable. * @type {Boolean} */ @property({ type: Boolean }) public clickable = false; /** * Do we need to add padding for a fab. * @type {Boolean} */ @property({ attribute: "has-fab", type: Boolean }) public hasFab = false; /** * Add an extra row at the bottom of the data table * @type {TemplateResult} */ @property({ attribute: false }) public appendRow?: TemplateResult; /** * Field with a unique id per entry in data. * @type {String} */ // eslint-disable-next-line lit/no-native-attributes @property({ type: String }) public id = "id"; /** * String to filter the data in the data table on. * @type {String} */ @property({ type: String }) public filter = ""; @property({ attribute: false }) public searchLabel?: string; /** * Number of active filters. * @type {Number} */ @property({ type: Number }) public filters?; /** * Number of current selections. * @type {Number} */ @property({ type: Number }) public selected?; /** * What path to use when the back button is pressed. * @type {String} * @attr back-path */ @property({ type: String, attribute: "back-path" }) public backPath?: string; /** * Function to call when the back button is pressed. * @type {() => void} */ @property({ attribute: false }) public backCallback?: () => void; /** * String to show when there are no records in the data table. * @type {String} */ @property({ attribute: false }) public noDataText?: string; /** * Hides the data table and show an empty message. * @type {Boolean} */ @property({ type: Boolean }) public empty = false; @property({ attribute: false }) public route!: Route; /** * Array of tabs to show on the page. * @type {Array} */ @property({ attribute: false }) public tabs: PageNavigation[] = []; /** * Show the filter menu. * @type {Boolean} */ @property({ attribute: "has-filters", type: Boolean }) public hasFilters = false; @property({ attribute: "show-filters", type: Boolean }) public showFilters = false; @property({ attribute: false }) public initialSorting?: { column: string; direction: SortingDirection; }; @property({ attribute: false }) public initialGroupColumn?: string; @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; @state() private _groupColumn?: string; @state() private _selectMode = false; @query("ha-data-table", true) private _dataTable!: HaDataTable; @query("search-input-outlined") private _searchInput!: HTMLElement; protected supportedShortcuts(): SupportedShortcuts { return { f: () => this._searchInput.focus(), }; } private _showPaneController = new ResizeController(this, { callback: (entries) => entries[0]?.contentRect.width > 750, }); public clearSelection() { this._dataTable.clearSelection(); } protected willUpdate() { if (this.hasUpdated) { return; } if (this.initialGroupColumn && this.columns[this.initialGroupColumn]) { this._setGroupColumn(this.initialGroupColumn); } if (this.initialSorting && this.columns[this.initialSorting.column]) { this._sortColumn = this.initialSorting.column; this._sortDirection = this.initialSorting.direction; } } protected render(): TemplateResult { const localize = this.localizeFunc || this.hass.localize; const showPane = this._showPaneController.value ?? !this.narrow; const filterButton = this.hasFilters ? html`
${this.filters ? html`
${this.filters}
` : nothing}
` : nothing; const selectModeBtn = this.selectable && !this._selectMode ? html` ` : nothing; const searchBar = html` `; const sortByMenu = Object.values(this.columns).find((col) => col.sortable) ? html` ${Object.entries(this.columns).map(([id, column]) => column.sortable ? html` ${this._sortColumn === id ? html` ` : nothing} ${column.title || column.label} ` : nothing )} ` : nothing; const groupByMenu = Object.values(this.columns).find((col) => col.groupable) ? html` ${Object.entries(this.columns).map(([id, column]) => column.groupable ? html` ${column.title || column.label} ` : nothing )} ${localize("ui.components.subpage-data-table.dont_group_by")} ${localize( "ui.components.subpage-data-table.collapse_all_groups" )} ${localize("ui.components.subpage-data-table.expand_all_groups")} ` : nothing; const settingsButton = html` `; return html` ${this._selectMode ? html`
${localize("ui.components.subpage-data-table.select_all")} ${localize("ui.components.subpage-data-table.select_none")} ${localize( "ui.components.subpage-data-table.exit_selection_mode" )} ${this.selected !== undefined ? html`

${localize("ui.components.subpage-data-table.selected", { selected: this.selected || "0", })}

` : nothing}
` : nothing} ${this.showFilters ? !showPane ? nothing : html`
${this.filters ? html`` : nothing}
` : nothing} ${this.empty ? html`
${this.noDataText}
` : html`
${this.narrow ? html`
${searchBar}
` : ""} ${!this.narrow ? html`
${this.hasFilters && !this.showFilters ? html`${filterButton}` : nothing}${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}${settingsButton}
` : html`
${this.hasFilters && !this.showFilters ? html`${filterButton}` : nothing} ${selectModeBtn}
${groupByMenu}${sortByMenu}${settingsButton}
`}
`}
${this.showFilters && !showPane ? html` ${localize("ui.components.subpage-data-table.filters")} ${this.filters ? html`` : nothing}
${localize("ui.components.subpage-data-table.show_results", { number: this.data.length, })}
` : nothing} `; } private _clearFilters() { fireEvent(this, "clear-filter"); } private _toggleFilters() { this.showFilters = !this.showFilters; } private _sortingChanged(ev) { this._sortDirection = ev.detail.direction; this._sortColumn = this._sortDirection ? ev.detail.column : undefined; } private _handleSortBy(ev: HaDropdownSelectEvent) { ev.preventDefault(); // keep the dropdown open const columnId = ev.detail.item.value; if (!this._sortDirection || this._sortColumn !== columnId) { this._sortDirection = "asc"; } else if (this._sortDirection === "asc") { this._sortDirection = "desc"; } else { this._sortDirection = "asc"; } this._sortColumn = columnId; fireEvent(this, "sorting-changed", { column: columnId, direction: this._sortDirection, }); } private _handleGroupBy(ev: HaDropdownSelectEvent) { const group = ev.detail.item.value; if (group === "reset") { this._setGroupColumn(""); return; } if (group === "collapse_all") { this._collapseAllGroups(); return; } if (group === "expand_all") { this._expandAllGroups(); return; } this._setGroupColumn(group); } private _setGroupColumn(columnId: string) { this._groupColumn = columnId; 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 }); }, localizeFunc: this.localizeFunc, }); } private _collapseAllGroups = () => { this._dataTable.collapseAllGroups(); }; private _expandAllGroups = () => { this._dataTable.expandAllGroups(); }; private _enableSelectMode() { this._selectMode = true; } private _handleSelect(ev: HaDropdownSelectEvent) { const action = ev.detail.item.value; if (!action) { return; } switch (action) { case "all": this._selectAll(); break; case "none": this._selectNone(); break; case "disable_select_mode": this._disableSelectMode(); break; } } private _disableSelectMode = () => { this._selectMode = false; this._dataTable.clearSelection(); }; private _selectAll = () => { this._dataTable.selectAll(); }; private _selectNone = () => { this._dataTable.clearSelection(); }; private _handleSearchChange(ev: CustomEvent) { if (this.filter === ev.detail.value) { return; } this.filter = ev.detail.value; fireEvent(this, "search-changed", { value: this.filter }); } static styles = css` :host { display: block; height: 100%; } ha-data-table { width: 100%; height: 100%; --data-table-border-width: 0; } :host(:not([narrow])) ha-data-table, .pane { height: calc( 100vh - 1px - var(--header-height, 0px) - var( --safe-area-inset-top, 0px ) - var(--safe-area-inset-bottom, 0px) ); display: block; } .pane-content { height: calc( 100vh - 1px - var(--header-height, 0px) - var(--header-height, 0px) - var( --safe-area-inset-top, 0px ) - var(--safe-area-inset-bottom, 0px) ); display: flex; flex-direction: column; } :host([narrow]) hass-tabs-subpage { --main-title-margin: 0; } :host([narrow]) { --expansion-panel-summary-padding: 0 16px; } .table-header { display: flex; align-items: center; --mdc-shape-small: 0; height: 56px; width: 100%; justify-content: space-between; padding: 0 16px; gap: var(--ha-space-4); box-sizing: border-box; background: var(--primary-background-color); border-bottom: 1px solid var(--divider-color); } search-input-outlined { flex: 1; } .search-toolbar { display: flex; align-items: center; color: var(--secondary-text-color); } .filters { --mdc-text-field-fill-color: var(--input-fill-color); --mdc-text-field-idle-line-color: var(--input-idle-line-color); --mdc-shape-small: 4px; --text-field-overflow: initial; display: flex; justify-content: flex-end; color: var(--primary-text-color); } .active-filters { color: var(--primary-text-color); position: relative; display: flex; align-items: center; padding: 2px 2px 2px 8px; margin-left: 4px; margin-inline-start: 4px; margin-inline-end: initial; font-size: var(--ha-font-size-m); width: max-content; cursor: initial; direction: var(--direction); } .active-filters ha-svg-icon { color: var(--primary-color); } .active-filters::before { background-color: var(--primary-color); opacity: 0.12; border-radius: var(--ha-border-radius-sm); position: absolute; top: 0; right: 0; bottom: 0; left: 0; content: ""; } .center { display: flex; align-items: center; justify-content: center; text-align: center; box-sizing: border-box; height: 100%; width: 100%; padding: 16px; } .badge { position: absolute; top: -4px; right: -4px; inset-inline-end: -4px; inset-inline-start: initial; min-width: 16px; box-sizing: border-box; border-radius: var(--ha-border-radius-circle); font-size: var(--ha-font-size-xs); font-weight: var(--ha-font-weight-normal); background-color: var(--primary-color); line-height: var(--ha-line-height-normal); text-align: center; padding: 0px 2px; color: var(--text-primary-color); } .narrow-header-row { display: flex; align-items: center; min-width: 100%; gap: var(--ha-space-4); padding: 0 16px; box-sizing: border-box; overflow-x: scroll; -ms-overflow-style: none; scrollbar-width: none; } .narrow-header-row .flex { flex: 1; margin-left: -16px; } .selection-bar { background: rgba(var(--rgb-primary-color), 0.1); width: 100%; height: 100%; display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; box-sizing: border-box; font-size: var(--ha-font-size-m); --ha-assist-chip-container-color: var(--card-background-color); } .selection-controls { display: flex; align-items: center; gap: var(--ha-space-2); } .selection-controls p { margin-left: 8px; margin-inline-start: 8px; margin-inline-end: initial; } .center-vertical { display: flex; align-items: center; gap: var(--ha-space-2); } .relative { position: relative; } ha-assist-chip { --ha-assist-chip-container-shape: 10px; --ha-assist-chip-container-color: var(--card-background-color); } .select-mode-chip { --md-assist-chip-icon-label-space: 0; --md-assist-chip-trailing-space: 8px; } ha-dialog { --mdc-dialog-min-width: 100vw; --mdc-dialog-max-width: 100vw; --mdc-dialog-min-height: 100%; --mdc-dialog-max-height: 100%; --vertical-align-dialog: flex-end; --ha-dialog-border-radius: var(--ha-border-radius-square); --dialog-content-padding: 0; } .filter-dialog-content { height: calc( 100vh - 70px - var(--header-height, 0px) - var( --safe-area-inset-top, 0px ) - var(--safe-area-inset-bottom, 0px) ); display: flex; flex-direction: column; } ha-dropdown ha-assist-chip { --md-assist-chip-trailing-space: 8px; } ha-dropdown-item.selected { border: 1px solid var(--primary-color); font-weight: var(--ha-font-weight-medium); color: var(--primary-color); background-color: var(--ha-color-fill-primary-quiet-resting); --icon-primary-color: var(--primary-color); } `; } declare global { interface HTMLElementTagNameMap { "hass-tabs-subpage-data-table": HaTabsSubpageDataTable; } // for fire event interface HASSDomEvents { "search-changed": { value: string }; "grouping-changed": { value: string }; "columns-changed": { columnOrder: string[] | undefined; hiddenColumns: string[] | undefined; }; "clear-filter": undefined; } }