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``
: nothing}
${this.empty
? html`
${this.noDataText}
`
: html`
${this.narrow
? html`
`
: ""}
${!this.narrow
? html`
`
: html`
`}
`}
${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;
}
}