mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-22 13:44:54 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f432c8548 | ||
|
|
5c85533cc1 | ||
|
|
a926083bfb | ||
|
|
05c2695aed | ||
|
|
ec1ea90b15 | ||
|
|
328ca06e9b | ||
|
|
2e4c486eaf | ||
|
|
f14fc9c9fe | ||
|
|
9153ce4945 | ||
|
|
b0bdf549d7 | ||
|
|
71425dfa5b |
No files matched your search
@@ -2,8 +2,9 @@ import type {
|
||||
ReactiveController,
|
||||
ReactiveControllerHost,
|
||||
} from "@lit/reactive-element/reactive-controller";
|
||||
import type { HASSDomEvent } from "../dom/fire_event";
|
||||
import { mainWindow } from "../dom/get_main_window";
|
||||
import { replaceCurrentUrl } from "../navigate";
|
||||
import { replaceCurrentUrl, type NavigateOptions } from "../navigate";
|
||||
import { constructUrlCurrentPath } from "../url/construct-url";
|
||||
import { currentPath } from "../url/current-path";
|
||||
import {
|
||||
@@ -29,8 +30,8 @@ export interface QueryParamsControllerOptions {
|
||||
* Keeps part of a page's state in the query string of the URL, so it can be
|
||||
* shared, bookmarked and reloaded. Writes replace the current history entry,
|
||||
* so Back leaves the page instead of undoing each change, and only touch the
|
||||
* config's params. The controller only acts while the URL is still the
|
||||
* page the host was connected on.
|
||||
* config's params. The controller only acts while the host is connected and
|
||||
* the URL is still the page it was first connected on.
|
||||
*/
|
||||
export class QueryParamsController<
|
||||
C extends QueryParamConfig,
|
||||
@@ -43,6 +44,8 @@ export class QueryParamsController<
|
||||
|
||||
private _hostUpdated = false;
|
||||
|
||||
private _connected = false;
|
||||
|
||||
constructor(
|
||||
private _host: ReactiveControllerHost,
|
||||
private _config: C,
|
||||
@@ -52,7 +55,10 @@ export class QueryParamsController<
|
||||
}
|
||||
|
||||
public hostConnected() {
|
||||
this._path = currentPath();
|
||||
// A cached page is reconnected on the way to another page, before its
|
||||
// router has moved on, so it keeps the page it was first connected on
|
||||
this._path ??= currentPath();
|
||||
this._connected = true;
|
||||
mainWindow.addEventListener("location-changed", this._navigated);
|
||||
mainWindow.addEventListener("popstate", this._navigated);
|
||||
|
||||
@@ -68,6 +74,9 @@ export class QueryParamsController<
|
||||
}
|
||||
|
||||
public hostDisconnected() {
|
||||
// A page that was left can still finish a lookup, while a new page on the
|
||||
// same path is showing
|
||||
this._connected = false;
|
||||
mainWindow.removeEventListener("location-changed", this._navigated);
|
||||
mainWindow.removeEventListener("popstate", this._navigated);
|
||||
this._cancelPending();
|
||||
@@ -117,11 +126,25 @@ export class QueryParamsController<
|
||||
}
|
||||
|
||||
private _isActive(): boolean {
|
||||
return this._options.enabled() && currentPath() === this._path;
|
||||
return (
|
||||
this._connected && this._options.enabled() && currentPath() === this._path
|
||||
);
|
||||
}
|
||||
|
||||
private _navigated = () => {
|
||||
private _navigated = (ev?: Event | HASSDomEvent<NavigateOptions>) => {
|
||||
this._cancelPending();
|
||||
// A router sends a path without a page, like /config/automation, on to
|
||||
// its default page after the host is connected, so it follows that
|
||||
const path = currentPath();
|
||||
|
||||
if (
|
||||
ev &&
|
||||
"detail" in ev &&
|
||||
ev.detail?.replace &&
|
||||
path.startsWith(`${this._path}/`)
|
||||
) {
|
||||
this._path = path;
|
||||
}
|
||||
// Leaving the page gets here too, before the router swaps the page
|
||||
if (this._isActive()) {
|
||||
this._options.navigated();
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import type {
|
||||
DataTableFilter,
|
||||
DataTableFilters,
|
||||
} from "../../data/data_table_filters";
|
||||
import { dataTableQueryParamConfig } from "./data-table-query-params";
|
||||
import {
|
||||
createQueryString,
|
||||
decodeQueryParams,
|
||||
hasQueryParams,
|
||||
type QueryParamConfig,
|
||||
type QueryParamValues,
|
||||
type SearchParamsSource,
|
||||
} from "./query-params";
|
||||
|
||||
// The filters of the devices table. Search, sorting and grouping are kept by
|
||||
// the data table, see data-table-query-params.
|
||||
export const devicesFilterQueryParamConfig = {
|
||||
list: [
|
||||
"status",
|
||||
"floor",
|
||||
"area",
|
||||
"domain",
|
||||
"label",
|
||||
"config_entry",
|
||||
"sub_entry",
|
||||
],
|
||||
} as const satisfies QueryParamConfig;
|
||||
|
||||
export type DevicesFilterQueryParams = QueryParamValues<
|
||||
typeof devicesFilterQueryParamConfig
|
||||
>;
|
||||
|
||||
const list = (filter: DataTableFilter | undefined): string[] | undefined =>
|
||||
Array.isArray(filter?.value) ? filter.value : undefined;
|
||||
|
||||
const floorAreas = (
|
||||
filter: DataTableFilter | undefined
|
||||
): Record<string, string[]> | undefined =>
|
||||
filter?.value && !Array.isArray(filter.value) ? filter.value : undefined;
|
||||
|
||||
export const devicesFilterQueryParams = (
|
||||
filters: DataTableFilters
|
||||
): DevicesFilterQueryParams => ({
|
||||
status: list(filters["ha-filter-states"]),
|
||||
floor: floorAreas(filters["ha-filter-floor-areas"])?.floors,
|
||||
area: floorAreas(filters["ha-filter-floor-areas"])?.areas,
|
||||
domain: list(filters["ha-filter-integrations"]),
|
||||
label: list(filters["ha-filter-labels"]),
|
||||
config_entry: list(filters.config_entry),
|
||||
sub_entry: list(filters.sub_entry),
|
||||
});
|
||||
|
||||
// The filters as the URL holds them, so values that filter the same, such as
|
||||
// no list and an empty one, or a list in another order, compare equal. A
|
||||
// filter list sends back what it was given in its own order.
|
||||
export const devicesFilterQueryString = (filters: DataTableFilters): string => {
|
||||
const params = devicesFilterQueryParams(filters);
|
||||
const sorted: DevicesFilterQueryParams = {};
|
||||
|
||||
for (const key of devicesFilterQueryParamConfig.list) {
|
||||
sorted[key] = params[key] && [...params[key]].sort();
|
||||
}
|
||||
|
||||
return createQueryString(sorted, devicesFilterQueryParamConfig);
|
||||
};
|
||||
|
||||
// What a link says to show, undefined when it has no table params so what was
|
||||
// picked before stays. A link replaces the search and the filters, ones left
|
||||
// behind would hide part of what it shows. The table applies its sorting and
|
||||
// grouping. The filters work out their items from the values.
|
||||
export interface DevicesLink {
|
||||
search: string;
|
||||
filters: DataTableFilters;
|
||||
}
|
||||
|
||||
export const devicesLinkFromUrl = (
|
||||
searchParams: SearchParamsSource
|
||||
): DevicesLink | undefined => {
|
||||
if (
|
||||
!hasQueryParams(searchParams, devicesFilterQueryParamConfig) &&
|
||||
!hasQueryParams(searchParams, dataTableQueryParamConfig)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const params = decodeQueryParams(searchParams, devicesFilterQueryParamConfig);
|
||||
|
||||
const { search } = decodeQueryParams(searchParams, dataTableQueryParamConfig);
|
||||
|
||||
const values = (key: keyof DevicesFilterQueryParams): string[] =>
|
||||
params[key]?.filter(Boolean) ?? [];
|
||||
|
||||
const filter = (value: DataTableFilter["value"]): DataTableFilter => ({
|
||||
value,
|
||||
items: undefined,
|
||||
});
|
||||
|
||||
const floors = values("floor");
|
||||
const areas = values("area");
|
||||
|
||||
return {
|
||||
search: search ?? "",
|
||||
filters: {
|
||||
"ha-filter-states": filter(values("status")),
|
||||
"ha-filter-floor-areas": filter(
|
||||
floors.length || areas.length ? { floors, areas } : undefined
|
||||
),
|
||||
"ha-filter-integrations": filter(values("domain")),
|
||||
"ha-filter-labels": filter(values("label")),
|
||||
config_entry: filter(values("config_entry")),
|
||||
sub_entry: filter(values("sub_entry")),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -120,7 +120,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const localize = this._params.localizeFunc || this._localize;
|
||||
const localize = this._localize;
|
||||
|
||||
const columns = this._sortedColumns(
|
||||
this._params.columns,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { DataTableColumnContainer } from "./ha-data-table";
|
||||
|
||||
export interface DataTableSettingsDialogParams {
|
||||
@@ -10,7 +9,6 @@ export interface DataTableSettingsDialogParams {
|
||||
) => void;
|
||||
hiddenColumns?: string[];
|
||||
columnOrder?: string[];
|
||||
localizeFunc?: LocalizeFunc;
|
||||
}
|
||||
|
||||
export const loadDataTableSettingsDialog = () =>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import "./ha-ripple";
|
||||
|
||||
@customElement("ha-tab")
|
||||
@@ -17,14 +16,7 @@ export class HaTab extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
tabindex="0"
|
||||
role="tab"
|
||||
aria-selected=${this.active}
|
||||
aria-label=${ifDefined(this.name)}
|
||||
aria-describedby=${ifDefined(this.badge ? "badge" : undefined)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
>
|
||||
<div>
|
||||
<span class="content">
|
||||
<span class="icon">
|
||||
<slot name="icon"></slot>
|
||||
@@ -37,7 +29,7 @@ export class HaTab extends LitElement {
|
||||
<span class="name">${this.name}</span>
|
||||
${
|
||||
this.badge
|
||||
? html`<span class="badge" id="badge">${this.badge}</span>`
|
||||
? html`<span class="badge">${this.badge}</span>`
|
||||
: nothing
|
||||
}
|
||||
</span>
|
||||
@@ -46,12 +38,6 @@ export class HaTab extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleKeyDown(ev: KeyboardEvent): void {
|
||||
if (ev.key === "Enter") {
|
||||
(ev.target as HTMLElement).click();
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
div {
|
||||
padding: 0;
|
||||
@@ -64,7 +50,6 @@ export class HaTab extends LitElement {
|
||||
height: var(--header-height);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -115,7 +100,7 @@ export class HaTab extends LitElement {
|
||||
background-color: var(--ha-color-fill-primary-loud-resting);
|
||||
}
|
||||
|
||||
/* Hidden from view but still read out through aria-describedby */
|
||||
/* Hidden from view but still read out as part of the link name */
|
||||
:host([narrow]) .badge {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
@@ -159,20 +144,6 @@ export class HaTab extends LitElement {
|
||||
:host(:not([narrow])) ::slotted([slot="icon"]) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div:focus-visible {
|
||||
background-color: rgba(var(--rgb-secondary-text-color), 0.08);
|
||||
}
|
||||
|
||||
@supports (color: color-mix(in srgb, black, transparent)) {
|
||||
div:focus-visible {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--secondary-text-color) 8%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
declare global {
|
||||
|
||||
@@ -204,12 +204,14 @@ export class TraceTree {
|
||||
const node = this._base(config, path, type, parentDisabled);
|
||||
const records = this.trace.trace[path] as ConditionTraceStep[] | undefined;
|
||||
const executed = !!records?.some((record) => record.result || record.error);
|
||||
// A disabled condition is still recorded by Core, with a `null` result
|
||||
// (or `enabled: false` in a sequence). It is skipped, so the run passes.
|
||||
node.condition = {
|
||||
executed,
|
||||
passed: !!records?.some((record) => record.result?.result),
|
||||
failed: !!records?.some(
|
||||
(record) => record.result && !record.result.result
|
||||
passed: !!records?.some(
|
||||
(record) => record.result && record.result.result !== false
|
||||
),
|
||||
failed: !!records?.some((record) => record.result?.result === false),
|
||||
};
|
||||
node.track = executed;
|
||||
return node;
|
||||
|
||||
+2
-1
@@ -32,7 +32,8 @@ export interface TriggerTraceStep extends BaseTraceStep {
|
||||
}
|
||||
|
||||
export interface ConditionTraceStep extends BaseTraceStep {
|
||||
result?: { result: boolean };
|
||||
// `null` when the condition is disabled.
|
||||
result?: { result: boolean | null };
|
||||
}
|
||||
|
||||
export interface CallServiceActionTraceStep extends BaseTraceStep {
|
||||
|
||||
@@ -21,7 +21,6 @@ import { canShowPage } from "../common/config/can_show_page";
|
||||
import { QueryParamsController } from "../common/controllers/query-params-controller";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { fireEvent, type HASSDomTargetEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
dataTableQueryParamConfig,
|
||||
dataTableQueryParamsFromState,
|
||||
@@ -62,8 +61,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
// Unread, kept for callers that still pass it until they move to contexts
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localizeFunc?: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig!: ContextType<typeof configContext>;
|
||||
@@ -340,7 +337,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const localize = this.localizeFunc || this._i18n.localize;
|
||||
const localize = this._i18n.localize;
|
||||
const showPane = this._showPaneController.value ?? !this.narrow;
|
||||
const filterButton = this.hasFilters
|
||||
? html`<ha-filter-pane-chip
|
||||
@@ -495,7 +492,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.localizeFunc=${this.localizeFunc}
|
||||
.isWide=${this.isWide}
|
||||
.backPath=${this.backPath}
|
||||
.backCallback=${this.backCallback}
|
||||
@@ -868,7 +864,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
this.hiddenColumns = hiddenColumns;
|
||||
fireEvent(this, "columns-changed", { columnOrder, hiddenColumns });
|
||||
},
|
||||
localizeFunc: this.localizeFunc,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,16 +9,19 @@ import {
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { restoreScroll } from "../common/decorators/restore-scroll";
|
||||
import type { HASSDomTargetEvent } from "../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import { getHistoryState, navigate } from "../common/navigate";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../common/translations/localize";
|
||||
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
|
||||
import { handleBackClick } from "./back-navigation";
|
||||
import "../components/ha-icon-button-arrow-prev";
|
||||
import "../components/ha-menu-button";
|
||||
import "../components/ha-svg-icon";
|
||||
@@ -29,8 +32,10 @@ import {
|
||||
internationalizationContext,
|
||||
narrowViewportContext,
|
||||
} from "../data/context";
|
||||
import type { CoreFrontendUserData } from "../data/frontend";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
import { handleBackClick } from "./back-navigation";
|
||||
|
||||
const normalizePathname = (pathname: string): string =>
|
||||
pathname.endsWith("/") && pathname.length > 1
|
||||
@@ -61,8 +66,6 @@ export class HassTabsSubpage extends LitElement {
|
||||
// Unread, kept for callers that still pass it until they move to contexts
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localizeFunc?: LocalizeFunc;
|
||||
|
||||
@property({ type: String, attribute: "back-path" }) public backPath?: string;
|
||||
|
||||
@property({ attribute: false }) public backCallback?: () => void;
|
||||
@@ -110,6 +113,10 @@ export class HassTabsSubpage extends LitElement {
|
||||
|
||||
@state() private _activeTab?: PageNavigation;
|
||||
|
||||
@state() private _hasCustomHeader = false;
|
||||
|
||||
@state() private _hasCustomToolbar = false;
|
||||
|
||||
@query(".content") private _content?: HTMLDivElement;
|
||||
|
||||
// @ts-ignore
|
||||
@@ -119,11 +126,11 @@ export class HassTabsSubpage extends LitElement {
|
||||
(
|
||||
tabs: PageNavigation[],
|
||||
activeTab: PageNavigation | undefined,
|
||||
_components,
|
||||
_language,
|
||||
_userData,
|
||||
_narrow,
|
||||
localizeFunc,
|
||||
_components: string[],
|
||||
_language: string,
|
||||
_userData: CoreFrontendUserData | undefined,
|
||||
_narrow: boolean,
|
||||
localize: LocalizeFunc,
|
||||
entities: ContextType<typeof entitiesContext>
|
||||
) => {
|
||||
const shownTabs = tabs.filter((page) =>
|
||||
@@ -134,9 +141,7 @@ export class HassTabsSubpage extends LitElement {
|
||||
this.showTabs = false;
|
||||
if (shownTabs.length === 1) {
|
||||
const page = shownTabs[0];
|
||||
return [
|
||||
page.translationKey ? localizeFunc(page.translationKey) : page.name,
|
||||
];
|
||||
return [this._tabName(page, localize)];
|
||||
}
|
||||
return [""];
|
||||
}
|
||||
@@ -144,16 +149,18 @@ export class HassTabsSubpage extends LitElement {
|
||||
this.showTabs = true;
|
||||
return shownTabs.map(
|
||||
(page) => html`
|
||||
<a href=${page.path} @click=${this._tabClicked}>
|
||||
<a
|
||||
href=${page.path}
|
||||
aria-current=${ifDefined(
|
||||
page.path === activeTab?.path ? "page" : undefined
|
||||
)}
|
||||
@click=${this._tabClicked}
|
||||
>
|
||||
<ha-tab
|
||||
.active=${page.path === activeTab?.path}
|
||||
.narrow=${this._narrow}
|
||||
.badge=${page.badge}
|
||||
.name=${
|
||||
page.translationKey
|
||||
? localizeFunc(page.translationKey)
|
||||
: page.name
|
||||
}
|
||||
.name=${this._tabName(page, localize)}
|
||||
>
|
||||
${
|
||||
page.iconPath
|
||||
@@ -183,6 +190,7 @@ export class HassTabsSubpage extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const localize = this._i18n.localize;
|
||||
const tabs = this._getTabs(
|
||||
this.tabs,
|
||||
this._activeTab,
|
||||
@@ -190,14 +198,19 @@ export class HassTabsSubpage extends LitElement {
|
||||
this._i18n.language,
|
||||
this._hassConfig.userData,
|
||||
this._narrow,
|
||||
this.localizeFunc || this._i18n.localize,
|
||||
localize,
|
||||
this._entities
|
||||
);
|
||||
const backPath = sanitizeNavigationPath(this.backPath);
|
||||
const hiddenHeading = this.showTabs
|
||||
? this._activeTab && this._tabName(this._activeTab, localize)
|
||||
: this._hasCustomHeader || this._hasCustomToolbar
|
||||
? (tabs[0] as string)
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<div class="toolbar ${classMap({ narrow: this._narrow })}">
|
||||
<slot name="toolbar">
|
||||
<slot name="toolbar" @slotchange=${this._toolbarSlotChanged}>
|
||||
<div class="toolbar-content">
|
||||
${
|
||||
this.mainPage || (!backPath && getHistoryState()?.root)
|
||||
@@ -212,7 +225,9 @@ export class HassTabsSubpage extends LitElement {
|
||||
${
|
||||
this._narrow || !this.showTabs
|
||||
? html`<div class="main-title">
|
||||
<slot name="header">${!this.showTabs ? tabs[0] : ""}</slot>
|
||||
<slot name="header" @slotchange=${this._headerSlotChanged}
|
||||
>${!this.showTabs ? html`<h1>${tabs[0]}</h1>` : ""}</slot
|
||||
>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
@@ -244,6 +259,11 @@ export class HassTabsSubpage extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
<div class="content ha-scrollbar" @scroll=${this._saveScrollPos}>
|
||||
${
|
||||
hiddenHeading
|
||||
? html`<h1 class="visually-hidden">${hiddenHeading}</h1>`
|
||||
: nothing
|
||||
}
|
||||
<slot></slot>
|
||||
${this.hasFab ? html`<div class="fab-bottom-space"></div>` : nothing}
|
||||
</div>
|
||||
@@ -268,6 +288,33 @@ export class HassTabsSubpage extends LitElement {
|
||||
this._content.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
private _headerSlotChanged(ev: Event): void {
|
||||
if (ev.target !== ev.currentTarget) {
|
||||
return;
|
||||
}
|
||||
this._hasCustomHeader =
|
||||
(ev.target as HTMLSlotElement).assignedNodes().length > 0;
|
||||
}
|
||||
|
||||
// slotchange bubbles, so nested slots also land here
|
||||
private _toolbarSlotChanged(ev: Event): void {
|
||||
if (ev.target !== ev.currentTarget) {
|
||||
return;
|
||||
}
|
||||
this._hasCustomToolbar =
|
||||
(ev.target as HTMLSlotElement).assignedNodes().length > 0;
|
||||
}
|
||||
|
||||
private _tabName(
|
||||
page: PageNavigation,
|
||||
localize: LocalizeFunc
|
||||
): string | undefined {
|
||||
return (
|
||||
(page.translationKey && localize(page.translationKey as LocalizeKeys)) ||
|
||||
page.name
|
||||
);
|
||||
}
|
||||
|
||||
private _backTapped(ev: MouseEvent): void {
|
||||
handleBackClick(ev, this.backPath, this.backCallback);
|
||||
}
|
||||
@@ -373,6 +420,32 @@ export class HassTabsSubpage extends LitElement {
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
#tabbar > a:focus-visible {
|
||||
outline: var(--wa-focus-ring);
|
||||
outline-offset: calc(-1 * var(--wa-focus-ring-width));
|
||||
background-color: rgba(var(--rgb-secondary-text-color), 0.08);
|
||||
}
|
||||
|
||||
@supports (color: color-mix(in srgb, black, transparent)) {
|
||||
#tabbar > a:focus-visible {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--secondary-text-color) 8%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#tabbar.bottom-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -418,6 +491,10 @@ export class HassTabsSubpage extends LitElement {
|
||||
.narrow .main-title {
|
||||
margin-inline-start: var(--main-title-margin, var(--ha-space-2));
|
||||
}
|
||||
.main-title h1 {
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
|
||||
@@ -255,14 +255,14 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
|
||||
this._filteredSchema(this.addon.options, this.addon.schema!)
|
||||
);
|
||||
return html`
|
||||
<h1>${this.addon.name}</h1>
|
||||
<h2 class="app-name">${this.addon.name}</h2>
|
||||
<ha-card outlined>
|
||||
<div class="header">
|
||||
<h2>
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.options.header"
|
||||
)}
|
||||
</h2>
|
||||
</h3>
|
||||
<div class="card-menu">
|
||||
<ha-dropdown @wa-select=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
@@ -547,11 +547,22 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
|
||||
ha-dropdown-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
.app-name {
|
||||
margin-block: var(--ha-space-4) var(--ha-space-2);
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
color: var(--primary-text-color);
|
||||
font-family: var(--ha-font-family-body);
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.header h2 {
|
||||
.header h3 {
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
|
||||
@@ -561,6 +572,9 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
|
||||
display: block;
|
||||
margin-block: 0px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.card-actions.right {
|
||||
justify-content: flex-end;
|
||||
|
||||
@@ -89,12 +89,14 @@ type AreaQuickLinkKey =
|
||||
const NAVIGATION_ACTIONS: {
|
||||
value: string;
|
||||
path: string;
|
||||
params?: string;
|
||||
icon: string;
|
||||
countKey: AreaQuickLinkKey;
|
||||
}[] = [
|
||||
{
|
||||
value: "navigate-devices",
|
||||
path: "/config/devices/dashboard",
|
||||
params: "&status=disabled",
|
||||
icon: mdiDevices,
|
||||
countKey: "devices",
|
||||
},
|
||||
@@ -852,7 +854,9 @@ class HaConfigAreaPage extends LitElement {
|
||||
|
||||
const navAction = NAVIGATION_ACTIONS.find((a) => a.value === action);
|
||||
if (navAction) {
|
||||
navigate(`${navAction.path}?historyBack=1&area=${this.areaId}`);
|
||||
navigate(
|
||||
`${navAction.path}?historyBack=1&area=${this.areaId}${navAction.params ?? ""}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -747,11 +747,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
!this.automations.length
|
||||
? html`<div class="empty" slot="empty">
|
||||
<ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon>
|
||||
<h1>
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.empty_header"
|
||||
)}
|
||||
</h1>
|
||||
</h2>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.empty_text_1"
|
||||
@@ -1626,8 +1626,14 @@ ${rejected
|
||||
.empty ha-button {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.empty h1 {
|
||||
.empty h2 {
|
||||
margin-block: var(--ha-space-5);
|
||||
font-family: var(--ha-font-family-heading);
|
||||
font-size: var(--ha-font-size-3xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { DataTableSettingsController } from "../../../common/controllers/data-table-settings-controller";
|
||||
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
@@ -24,12 +26,15 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import {
|
||||
getHistoryState,
|
||||
navigate,
|
||||
updateHistoryState,
|
||||
} from "../../../common/navigate";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import {
|
||||
devicesFilterQueryParamConfig,
|
||||
devicesFilterQueryParams,
|
||||
devicesFilterQueryString,
|
||||
devicesLinkFromUrl,
|
||||
} from "../../../common/url/devices-query-params";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
@@ -38,7 +43,6 @@ import type {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
@@ -132,8 +136,6 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _selected: string[] = [];
|
||||
|
||||
private _selectedCanDelete: string[] = [];
|
||||
@@ -141,14 +143,11 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "devices-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter: string = getHistoryState()?.filter || "";
|
||||
private _tableSettings = new DataTableSettingsController(this, {
|
||||
search: "devices-table-search",
|
||||
sorting: "devices-table-sort",
|
||||
grouping: "devices-table-grouping",
|
||||
});
|
||||
|
||||
@state()
|
||||
private _filters: DataTableFilters = {};
|
||||
@@ -169,12 +168,6 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
@state()
|
||||
_labels?: LabelRegistryEntry[];
|
||||
|
||||
@storage({ key: "devices-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "devices-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({ key: "devices-table-collapsed", state: false, subscribe: false })
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@@ -196,39 +189,16 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
|
||||
private _ignoreLocationChange = false;
|
||||
|
||||
private _fromUrl = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("location-changed", this._locationChanged);
|
||||
window.addEventListener("popstate", this._popState);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("location-changed", this._locationChanged);
|
||||
window.removeEventListener("popstate", this._popState);
|
||||
}
|
||||
|
||||
private _locationChanged = () => {
|
||||
if (this._ignoreLocationChange) {
|
||||
this._ignoreLocationChange = false;
|
||||
return;
|
||||
// The filters are kept in the URL by the page, the search, sorting and
|
||||
// grouping by the table
|
||||
private _filterParams = new QueryParamsController(
|
||||
this,
|
||||
devicesFilterQueryParamConfig,
|
||||
{
|
||||
enabled: () => true,
|
||||
navigated: () => this._applyLink(),
|
||||
}
|
||||
if (window.location.search.substring(1) !== this._searchParms.toString()) {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
this._setFiltersFromUrl();
|
||||
}
|
||||
};
|
||||
|
||||
private _popState = () => {
|
||||
if (window.location.search.substring(1) !== this._searchParms.toString()) {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
this._setFiltersFromUrl();
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
private _states = memoizeOne((localize: LocalizeFunc) => [
|
||||
{
|
||||
@@ -241,7 +211,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this.hasUpdated) {
|
||||
this._filters = this._storageFilters;
|
||||
this._setFiltersFromUrl();
|
||||
this._applyLink();
|
||||
}
|
||||
if (changedProps.has("_selected") || changedProps.has("entries")) {
|
||||
this._selectedCanDelete = this._selected.filter((d) => {
|
||||
@@ -256,60 +226,45 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _setFiltersFromUrl() {
|
||||
const area = this._searchParms.get("area");
|
||||
const domain = this._searchParms.get("domain");
|
||||
const configEntry = this._searchParms.get("config_entry");
|
||||
const subEntry = this._searchParms.get("sub_entry");
|
||||
const label = this._searchParms.has("label");
|
||||
// Links, like the ones on an integration page, say in the URL what to show.
|
||||
// The router keeps the page open, so a link can land on it.
|
||||
private _applyLink() {
|
||||
const searchParams = this._filterParams.searchParams();
|
||||
const link = searchParams ? devicesLinkFromUrl(searchParams) : undefined;
|
||||
|
||||
// Shown, but not stored as the user's choice
|
||||
if (link) {
|
||||
this._tableSettings.showSearch(link.search);
|
||||
this._filters = this._keepItems(link.filters);
|
||||
} else if (searchParams?.toString() === "") {
|
||||
// Opened again without params, like from the sidebar. Written once the
|
||||
// table has seen the URL without params, so it restores what it stored
|
||||
this._filters = this._keepItems(this._storageFilters);
|
||||
queueMicrotask(() => this._writeFilters());
|
||||
|
||||
if (!area && !domain && !configEntry && !label) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._fromUrl = true;
|
||||
this._filter = getHistoryState()?.filter || "";
|
||||
|
||||
this._filters = {
|
||||
"ha-filter-states": {
|
||||
value: [
|
||||
...((this._filters["ha-filter-states"]?.value as string[]) || []),
|
||||
"disabled",
|
||||
],
|
||||
items: undefined,
|
||||
},
|
||||
"ha-filter-floor-areas": {
|
||||
value: area ? { areas: [area] } : undefined,
|
||||
items: undefined,
|
||||
},
|
||||
"ha-filter-integrations": {
|
||||
value: domain ? [domain] : [],
|
||||
items: undefined,
|
||||
},
|
||||
config_entry: {
|
||||
value: configEntry ? [configEntry] : [],
|
||||
items: undefined,
|
||||
},
|
||||
sub_entry: {
|
||||
value: subEntry ? [subEntry] : [],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._filterLabel();
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _filterLabel() {
|
||||
const label = this._searchParms.get("label");
|
||||
if (!label) {
|
||||
return;
|
||||
}
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-labels": {
|
||||
value: [label],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
// The filters only work out their items again when their value changes, so
|
||||
// keep the items of the ones that stay as they are
|
||||
private _keepItems(filters: DataTableFilters): DataTableFilters {
|
||||
return Object.fromEntries(
|
||||
Object.entries(filters).map(([key, filter]) => {
|
||||
const current = this._filters[key];
|
||||
|
||||
return [
|
||||
key,
|
||||
current && deepEqual(filter.value, current.value) ? current : filter,
|
||||
];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _writeFilters() {
|
||||
this._filterParams.write(devicesFilterQueryParams(this._filters));
|
||||
}
|
||||
|
||||
private _reloadConfigEntries() {
|
||||
@@ -318,9 +273,8 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
private _clearFilter() {
|
||||
this._filters = {};
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = {};
|
||||
}
|
||||
this._storageFilters = {};
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _devicesAndFilterDomains = memoizeOne(
|
||||
@@ -399,24 +353,29 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
Array.isArray(filter.value) &&
|
||||
filter.value.length
|
||||
) {
|
||||
if (!(
|
||||
Array.isArray(this._filters.config_entry?.value) &&
|
||||
this._filters.config_entry.value.length === 1
|
||||
)) {
|
||||
const configEntryIds = this._filters.config_entry?.value;
|
||||
const subEntryIds = filter.value;
|
||||
|
||||
if (
|
||||
!Array.isArray(configEntryIds) ||
|
||||
!configEntryIds.length ||
|
||||
!Array.isArray(subEntryIds)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const configEntryId = this._filters.config_entry.value[0];
|
||||
outputDevices = outputDevices.filter(
|
||||
(device) =>
|
||||
device.config_entries_subentries[configEntryId] &&
|
||||
(filter.value as string[]).some((subEntryId) =>
|
||||
device.config_entries_subentries[configEntryId].includes(
|
||||
subEntryId
|
||||
)
|
||||
|
||||
outputDevices = outputDevices.filter((device) =>
|
||||
configEntryIds.some((configEntryId) =>
|
||||
device.config_entries_subentries[configEntryId]?.some(
|
||||
(subEntryId) =>
|
||||
subEntryId !== null && subEntryIds.includes(subEntryId)
|
||||
)
|
||||
)
|
||||
);
|
||||
if (!this._subEntries) {
|
||||
this._loadSubEntries(configEntryId);
|
||||
|
||||
// The titles shown in the filter chip need a single config entry
|
||||
if (configEntryIds.length === 1 && !this._subEntries) {
|
||||
this._loadSubEntries(configEntryIds[0]);
|
||||
}
|
||||
} else if (
|
||||
key === "ha-filter-integrations" &&
|
||||
@@ -654,6 +613,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
urlName: "integration",
|
||||
minWidth: "120px",
|
||||
template: (device) =>
|
||||
!this.entries
|
||||
@@ -669,6 +629,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
urlName: "parent",
|
||||
defaultHidden: true,
|
||||
minWidth: "120px",
|
||||
template: (device) => device.parent_device_name || "",
|
||||
@@ -678,6 +639,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
urlName: "manufacturer",
|
||||
minWidth: "120px",
|
||||
},
|
||||
model: {
|
||||
@@ -875,7 +837,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
.filter=${this._filter}
|
||||
.filter=${this._tableSettings.search}
|
||||
has-filters
|
||||
.filters=${
|
||||
Object.values(this._filters).filter((filter) =>
|
||||
@@ -887,19 +849,20 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
)
|
||||
).length
|
||||
}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialGroupColumn=${this._tableSettings.grouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.initialSorting=${this._tableSettings.sorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@clear-filter=${this._clearFilter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@search-changed=${this._tableSettings.searchChanged}
|
||||
@sorting-changed=${this._tableSettings.sortingChanged}
|
||||
@grouping-changed=${this._tableSettings.groupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@row-click=${this._handleRowClicked}
|
||||
clickable
|
||||
sync-url
|
||||
has-fab
|
||||
class=${this.narrow ? "narrow" : ""}
|
||||
>
|
||||
@@ -1093,10 +1056,27 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
private _filterChanged(ev) {
|
||||
const type = ev.target.localName;
|
||||
this._filters = { ...this._filters, [type]: ev.detail };
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = this._filters;
|
||||
const filters = { ...this._filters, [type]: ev.detail };
|
||||
|
||||
// A filter can still finish looking up its items after the page is left.
|
||||
// Keep them for when the page comes back, but leave the URL alone.
|
||||
if (!this.isConnected) {
|
||||
this._filters = filters;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// A filter also sends the value it was given, once it has found its items.
|
||||
// Only a change to what the URL holds is the user's, so a link isn't stored.
|
||||
if (
|
||||
devicesFilterQueryString(filters) !==
|
||||
devicesFilterQueryString(this._filters)
|
||||
) {
|
||||
this._storageFilters = { ...this._storageFilters, [type]: ev.detail };
|
||||
}
|
||||
|
||||
this._filters = filters;
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _batteryEntity(
|
||||
@@ -1123,15 +1103,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const deviceId = ev.detail.id;
|
||||
this._ignoreLocationChange = true;
|
||||
navigate(`/config/devices/device/${deviceId}`);
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
updateHistoryState({ filter: this._filter });
|
||||
}
|
||||
|
||||
private _addDevice() {
|
||||
const { filteredConfigEntry, filteredDomains } =
|
||||
this._devicesAndFilterDomains(
|
||||
@@ -1157,8 +1131,13 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const integrations = this._filters["ha-filter-integrations"]?.value;
|
||||
showAddIntegrationDialog(this, {
|
||||
domain: this._searchParms.get("domain") || undefined,
|
||||
domain:
|
||||
Array.isArray(integrations) && integrations.length === 1
|
||||
? integrations[0]
|
||||
: undefined,
|
||||
navigateToResult: true,
|
||||
});
|
||||
}
|
||||
@@ -1319,14 +1298,6 @@ ${rejected
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ export class HaConfigEntryRow extends LitElement {
|
||||
href=${
|
||||
allDevices.length === 1
|
||||
? `/config/devices/device/${allDevices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}&status=disabled`
|
||||
}
|
||||
>
|
||||
<ha-dropdown-item value="devices">
|
||||
@@ -278,7 +278,7 @@ export class HaConfigEntryRow extends LitElement {
|
||||
href=${
|
||||
allServices.length === 1
|
||||
? `/config/devices/device/${allServices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}&status=disabled`
|
||||
}
|
||||
>
|
||||
<ha-dropdown-item value="services">
|
||||
|
||||
@@ -617,7 +617,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
href=${
|
||||
devices.length === 1
|
||||
? `/config/devices/device/${devices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&domain=${this.domain}`
|
||||
: `/config/devices/dashboard?historyBack=1&domain=${this.domain}&status=disabled`
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -635,7 +635,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
href=${
|
||||
services.length === 1
|
||||
? `/config/devices/device/${services[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&domain=${this.domain}`
|
||||
: `/config/devices/dashboard?historyBack=1&domain=${this.domain}&status=disabled`
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
|
||||
@@ -113,7 +113,7 @@ class HaConfigSubEntryRow extends LitElement {
|
||||
href=${
|
||||
devices.length === 1
|
||||
? `/config/devices/device/${devices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}&status=disabled`
|
||||
}
|
||||
>
|
||||
<ha-dropdown-item value="devices">
|
||||
@@ -138,7 +138,7 @@ class HaConfigSubEntryRow extends LitElement {
|
||||
href=${
|
||||
services.length === 1
|
||||
? `/config/devices/device/${services[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}&status=disabled`
|
||||
}
|
||||
>
|
||||
<ha-dropdown-item value="services">
|
||||
|
||||
@@ -178,7 +178,7 @@ export class MatterConfigDashboard extends LitElement {
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}&status=disabled`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
|
||||
@@ -221,7 +221,7 @@ export class MQTTConfigPanel extends LitElement {
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}&status=disabled`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
|
||||
@@ -237,7 +237,7 @@ class ZHAConfigDashboard extends LitElement {
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}&status=disabled`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export class DialogZWaveJSAddNode extends HTMLElement {
|
||||
|
||||
private async _openDialog() {
|
||||
await navigate(
|
||||
`/config/devices/dashboard?config_entry=${this.configEntryId}`,
|
||||
`/config/devices/dashboard?config_entry=${this.configEntryId}&status=disabled`,
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
|
||||
+1
-1
@@ -326,7 +326,7 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}&status=disabled`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
|
||||
@@ -468,7 +468,7 @@ export class HaConfigLabels extends LitElement {
|
||||
this._navigateConfig("/config/entities");
|
||||
break;
|
||||
case "navigate-devices":
|
||||
this._navigateConfig("/config/devices/dashboard");
|
||||
this._navigateConfig("/config/devices/dashboard", "&status=disabled");
|
||||
break;
|
||||
case "navigate-automations":
|
||||
this._navigateConfig("/config/automation/dashboard");
|
||||
@@ -485,8 +485,10 @@ export class HaConfigLabels extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _navigateConfig = (path: string) => {
|
||||
navigate(`${path}?historyBack=1&label=${this._overflowLabel.label_id}`);
|
||||
private _navigateConfig = (path: string, params = "") => {
|
||||
navigate(
|
||||
`${path}?historyBack=1&label=${this._overflowLabel.label_id}${params}`
|
||||
);
|
||||
};
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
|
||||
@@ -95,8 +95,6 @@ const SEARCH_CONTEXT_LINES = 3;
|
||||
|
||||
@customElement("error-log-card")
|
||||
class ErrorLogCard extends LitElement {
|
||||
@property({ attribute: false }) public localizeFunc?: LocalizeFunc<any>;
|
||||
|
||||
@property() public filter = "";
|
||||
|
||||
@property() public header?: string;
|
||||
@@ -194,7 +192,7 @@ class ErrorLogCard extends LitElement {
|
||||
const hasBoots =
|
||||
!integrationLog && this._streamSupported && Array.isArray(this._boots);
|
||||
|
||||
const localize = this.localizeFunc || this._localize;
|
||||
const localize = this._localize;
|
||||
const logFileDownloadUnavailableReason =
|
||||
!integrationLog && (!this.provider || this.provider === "core")
|
||||
? getCoreLogFileDownloadUnavailableReason(this._config)
|
||||
@@ -473,10 +471,7 @@ class ErrorLogCard extends LitElement {
|
||||
this.filter)
|
||||
) {
|
||||
this._debounceSearch();
|
||||
} else if (
|
||||
(changedProps.has("_localize") || changedProps.has("localizeFunc")) &&
|
||||
this.filter
|
||||
) {
|
||||
} else if (changedProps.has("_localize") && this.filter) {
|
||||
// The hidden lines buttons are built outside Lit, so relabel them.
|
||||
this._ansiToHtmlElement?.filterLines(this.filter);
|
||||
}
|
||||
@@ -736,13 +731,10 @@ class ErrorLogCard extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = (this.localizeFunc || this._localize)(
|
||||
"ui.panel.config.logs.failed_get_logs",
|
||||
{
|
||||
provider: this.provider,
|
||||
error: extractApiErrorMessage(err),
|
||||
}
|
||||
);
|
||||
this._error = this._localize("ui.panel.config.logs.failed_get_logs", {
|
||||
provider: this.provider,
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,7 +813,7 @@ class ErrorLogCard extends LitElement {
|
||||
}
|
||||
this._unsubscribeIntegrationLog();
|
||||
this._loadingState = "loaded";
|
||||
this._error = (this.localizeFunc || this._localize)(
|
||||
this._error = this._localize(
|
||||
"ui.panel.config.logs.integration_log_unsupported"
|
||||
);
|
||||
}
|
||||
@@ -874,10 +866,7 @@ class ErrorLogCard extends LitElement {
|
||||
}, 150);
|
||||
|
||||
private _expandLabel = (count: number) =>
|
||||
(this.localizeFunc || this._localize)(
|
||||
"ui.panel.config.logs.show_hidden_lines",
|
||||
{ count }
|
||||
);
|
||||
this._localize("ui.panel.config.logs.show_hidden_lines", { count });
|
||||
|
||||
private _debounceScrollToBottom = debounce(() => {
|
||||
this._logElement!.scrollTop = this._logElement!.scrollHeight;
|
||||
|
||||
@@ -692,11 +692,11 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
!this.scenes.length
|
||||
? html`<div class="empty" slot="empty">
|
||||
<ha-svg-icon .path=${mdiPalette}></ha-svg-icon>
|
||||
<h1>
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.empty_header"
|
||||
)}
|
||||
</h1>
|
||||
</h2>
|
||||
<p>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.empty_text")}
|
||||
</p>
|
||||
@@ -1319,8 +1319,14 @@ ${rejected
|
||||
.empty ha-button {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.empty h1 {
|
||||
.empty h2 {
|
||||
margin-block: var(--ha-space-5);
|
||||
font-family: var(--ha-font-family-heading);
|
||||
font-size: var(--ha-font-size-3xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
|
||||
@@ -22,8 +22,8 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
@@ -673,11 +673,11 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
!this.scripts.length
|
||||
? html` <div class="empty" slot="empty">
|
||||
<ha-svg-icon .path=${mdiScriptText}></ha-svg-icon>
|
||||
<h1>
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.picker.empty_header"
|
||||
)}
|
||||
</h1>
|
||||
</h2>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.picker.empty_text"
|
||||
@@ -1381,8 +1381,14 @@ ${rejected
|
||||
.empty ha-button {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.empty h1 {
|
||||
.empty h2 {
|
||||
margin-block: var(--ha-space-5);
|
||||
font-family: var(--ha-font-family-heading);
|
||||
font-size: var(--ha-font-size-3xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { DataTableSettingsController } from "../../../common/controllers/data-table-settings-controller";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { hasRejectedItems } from "../../../common/util/promise-all-settled-results";
|
||||
@@ -76,9 +76,14 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
return this.hass.auth.external?.config.canWriteTag;
|
||||
}
|
||||
|
||||
private _tableSettings = new DataTableSettingsController(this, {
|
||||
search: "tags-table-search",
|
||||
});
|
||||
@state()
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "tags-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
private _columns = memoizeOne((localize: LocalizeFunc) => {
|
||||
const columns: DataTableColumnContainer<TagRowData> = {
|
||||
@@ -94,7 +99,6 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
title: localize("ui.panel.config.tag.headers.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
urlName: "name",
|
||||
filterable: true,
|
||||
flex: 2,
|
||||
},
|
||||
@@ -107,7 +111,6 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
last_scanned_datetime: {
|
||||
title: localize("ui.panel.config.tag.headers.last_scanned"),
|
||||
sortable: true,
|
||||
urlName: "last_scanned",
|
||||
direction: "desc",
|
||||
template: (tag) => html`
|
||||
${
|
||||
@@ -215,9 +218,8 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
: undefined
|
||||
}
|
||||
@retry-load=${this._retryFetchTags}
|
||||
.filter=${this._tableSettings.search}
|
||||
@search-changed=${this._tableSettings.searchChanged}
|
||||
sync-url
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
@@ -415,6 +417,10 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -721,7 +721,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
this.columnOrder = columnOrder;
|
||||
this.hiddenColumns = hiddenColumns;
|
||||
},
|
||||
localizeFunc: this._i18n.localize,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -95,16 +95,16 @@ export class HaMarketplaceDiscover extends LitElement {
|
||||
|
||||
return html`<div class="content">
|
||||
<div class="intro">
|
||||
<h1>${localize("ui.panel.marketplace.discover.title")}</h1>
|
||||
<h2>${localize("ui.panel.marketplace.discover.title")}</h2>
|
||||
<p>${localize("ui.panel.marketplace.discover.intro")}</p>
|
||||
</div>
|
||||
${this._sections(this.marketplace.repositories).map(
|
||||
(section) =>
|
||||
html`<section class=${section.name}>
|
||||
<div class="section-header">
|
||||
<h2>
|
||||
<h3>
|
||||
${localize(`ui.panel.marketplace.discover.${section.name}`)}
|
||||
</h2>
|
||||
</h3>
|
||||
<a class="see-all" href=${section.seeAll}>
|
||||
${localize("ui.panel.marketplace.discover.see_all")}
|
||||
</a>
|
||||
@@ -184,10 +184,14 @@ export class HaMarketplaceDiscover extends LitElement {
|
||||
padding: var(--ha-space-8) var(--ha-space-4);
|
||||
}
|
||||
|
||||
h1 {
|
||||
.intro h2 {
|
||||
margin: 0;
|
||||
font-family: var(--ha-font-family-heading);
|
||||
font-size: var(--ha-font-size-3xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.intro p {
|
||||
@@ -202,10 +206,13 @@ export class HaMarketplaceDiscover extends LitElement {
|
||||
margin-block-end: var(--ha-space-3);
|
||||
}
|
||||
|
||||
h2 {
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.see-all {
|
||||
|
||||
@@ -339,7 +339,6 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
if (this.tab === "discover") {
|
||||
return html`<hass-tabs-subpage
|
||||
.tabs=${tabs}
|
||||
.localizeFunc=${this._i18n.localize}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
@@ -373,7 +372,6 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
{ number: repositories.length }
|
||||
)}
|
||||
.isWide=${this.isWide}
|
||||
.localizeFunc=${this._i18n.localize}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
@@ -424,9 +422,9 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
? nothing
|
||||
: html`<div class="empty" slot="empty">
|
||||
<ha-svg-icon .path=${mdiStore}></ha-svg-icon>
|
||||
<h1>
|
||||
<h2>
|
||||
${this._i18n.localize("ui.panel.marketplace.dashboard.empty_header")}
|
||||
</h1>
|
||||
</h2>
|
||||
<p>
|
||||
${this._i18n.localize("ui.panel.marketplace.dashboard.empty_text")}
|
||||
</p>
|
||||
@@ -1100,8 +1098,14 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
.empty ha-button {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.empty h1 {
|
||||
.empty h2 {
|
||||
margin-block: var(--ha-space-5);
|
||||
font-family: var(--ha-font-family-heading);
|
||||
font-size: var(--ha-font-size-3xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -78,11 +78,19 @@ const lorawanDevicesParams = {
|
||||
connection: "string?",
|
||||
} satisfies Redirect["params"];
|
||||
|
||||
// The tags table reads these from the link
|
||||
const tagsTableParams = {
|
||||
// The devices table reads these from the link
|
||||
const devicesTableParams = {
|
||||
search: "string?",
|
||||
sort: "string?",
|
||||
direction: "string?",
|
||||
group: "string?",
|
||||
status: "string?",
|
||||
floor: "string?",
|
||||
area: "string?",
|
||||
domain: "string?",
|
||||
label: "string?",
|
||||
config_entry: "string?",
|
||||
sub_entry: "string?",
|
||||
} satisfies Redirect["params"];
|
||||
|
||||
export const getMyRedirects = (): Redirects => ({
|
||||
@@ -285,6 +293,7 @@ export const getMyRedirects = (): Redirects => ({
|
||||
},
|
||||
devices: {
|
||||
redirect: "/config/devices/dashboard",
|
||||
params: devicesTableParams,
|
||||
},
|
||||
entities: {
|
||||
redirect: "/config/entities",
|
||||
@@ -337,7 +346,6 @@ export const getMyRedirects = (): Redirects => ({
|
||||
tags: {
|
||||
component: "tag",
|
||||
redirect: "/config/tags",
|
||||
params: tagsTableParams,
|
||||
},
|
||||
voice_assistants: {
|
||||
redirect: "/config/voice-assistants",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type Mock,
|
||||
} from "vitest";
|
||||
import { QueryParamsController } from "../../../src/common/controllers/query-params-controller";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
|
||||
const config = { string: ["search", "sort"] } as const;
|
||||
|
||||
@@ -98,6 +99,15 @@ describe("QueryParamsController", () => {
|
||||
expect(currentUrl()).toBe("/page?other=1");
|
||||
});
|
||||
|
||||
it("does nothing once disconnected, also on the same page", () => {
|
||||
// A page left while a lookup runs, with a new page on the same path
|
||||
controller.hostDisconnected();
|
||||
setUrl("/page?search=new");
|
||||
controller.write({ search: "old" });
|
||||
expect(currentUrl()).toBe("/page?search=new");
|
||||
expect(controller.searchParams()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does nothing once the URL is another page", () => {
|
||||
setUrl("/other-page?other=1");
|
||||
window.dispatchEvent(new Event("location-changed"));
|
||||
@@ -124,4 +134,30 @@ describe("QueryParamsController", () => {
|
||||
controller.hostConnected();
|
||||
expect(navigated).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("stays on its page when reconnected on the way to another page", () => {
|
||||
controller.hostUpdated();
|
||||
controller.hostDisconnected();
|
||||
setUrl("/other-page");
|
||||
controller.hostConnected();
|
||||
controller.write({ search: "abc" });
|
||||
expect(currentUrl()).toBe("/other-page");
|
||||
expect(navigated).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("follows a router sending its path on to the default page", () => {
|
||||
setUrl("/page/dashboard?other=1");
|
||||
fireEvent(window, "location-changed", { replace: true });
|
||||
expect(navigated).toHaveBeenCalledOnce();
|
||||
|
||||
controller.write({ search: "abc" });
|
||||
expect(currentUrl()).toBe("/page/dashboard?other=1&search=abc");
|
||||
});
|
||||
|
||||
it("does not follow a new entry for a page below its own", () => {
|
||||
setUrl("/page/detail");
|
||||
fireEvent(window, "location-changed", { replace: false });
|
||||
controller.write({ search: "abc" });
|
||||
expect(currentUrl()).toBe("/page/detail");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
devicesFilterQueryParamConfig,
|
||||
devicesFilterQueryParams,
|
||||
devicesLinkFromUrl,
|
||||
} from "../../../src/common/url/devices-query-params";
|
||||
import { createQueryString } from "../../../src/common/url/query-params";
|
||||
|
||||
describe("devices query params", () => {
|
||||
it("reads nothing without table params", () => {
|
||||
expect(devicesLinkFromUrl("?historyBack=1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("replaces every filter and the search with what a link asks for", () => {
|
||||
expect(devicesLinkFromUrl("?config_entry=abc&label=a,b")).toEqual({
|
||||
search: "",
|
||||
filters: {
|
||||
"ha-filter-states": { value: [], items: undefined },
|
||||
"ha-filter-floor-areas": { value: undefined, items: undefined },
|
||||
"ha-filter-integrations": { value: [], items: undefined },
|
||||
"ha-filter-labels": { value: ["a", "b"], items: undefined },
|
||||
config_entry: { value: ["abc"], items: undefined },
|
||||
sub_entry: { value: [], items: undefined },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("round trips the filters", () => {
|
||||
const filters = {
|
||||
"ha-filter-states": { value: ["disabled"], items: undefined },
|
||||
"ha-filter-floor-areas": {
|
||||
value: { floors: ["ground"], areas: ["kitchen"] },
|
||||
items: undefined,
|
||||
},
|
||||
"ha-filter-integrations": { value: ["hue"], items: undefined },
|
||||
"ha-filter-labels": { value: ["a"], items: undefined },
|
||||
config_entry: { value: ["abc"], items: undefined },
|
||||
sub_entry: { value: ["def"], items: undefined },
|
||||
};
|
||||
|
||||
expect(
|
||||
devicesLinkFromUrl(
|
||||
createQueryString(
|
||||
devicesFilterQueryParams(filters),
|
||||
devicesFilterQueryParamConfig
|
||||
)
|
||||
)
|
||||
).toEqual({ search: "", filters });
|
||||
});
|
||||
|
||||
it("writes the values, not the items the filters found", () => {
|
||||
expect(
|
||||
devicesFilterQueryParams({
|
||||
"ha-filter-floor-areas": {
|
||||
value: { areas: ["kitchen"] },
|
||||
items: new Set(["device_1"]),
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
status: undefined,
|
||||
floor: undefined,
|
||||
area: ["kitchen"],
|
||||
domain: undefined,
|
||||
label: undefined,
|
||||
config_entry: undefined,
|
||||
sub_entry: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import "../../src/components/ha-tab";
|
||||
import type { HaTab } from "../../src/components/ha-tab";
|
||||
|
||||
vi.mock("../../src/components/ha-ripple", () => ({}));
|
||||
|
||||
const openTab = async (badge?: string) => {
|
||||
const tab = document.createElement("ha-tab") as HaTab;
|
||||
tab.name = "Installed";
|
||||
tab.badge = badge;
|
||||
document.body.append(tab);
|
||||
await tab.updateComplete;
|
||||
return tab.shadowRoot!.querySelector('[role="tab"]')!;
|
||||
};
|
||||
|
||||
describe("ha-tab", () => {
|
||||
afterEach(() => document.body.replaceChildren());
|
||||
|
||||
it("tells a screen reader about its badge too", async () => {
|
||||
const tab = await openTab("3 updates");
|
||||
|
||||
const describedBy = tab.getAttribute("aria-describedby")!;
|
||||
expect(tab.querySelector(`#${describedBy}`)!.textContent).toBe("3 updates");
|
||||
});
|
||||
|
||||
it("describes nothing without a badge", async () => {
|
||||
const tab = await openTab();
|
||||
|
||||
expect(tab.hasAttribute("aria-describedby")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -572,6 +572,29 @@ describe("TraceTree branch completion", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each([{ result: null }, { enabled: false }])(
|
||||
"passes through a disabled condition: %j",
|
||||
(result) => {
|
||||
const tree = new TraceTree(
|
||||
createTrace(
|
||||
[
|
||||
{
|
||||
condition: "template",
|
||||
value_template: "{{ false }}",
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
[{ path: "sequence/0", timestamp, result }]
|
||||
)
|
||||
);
|
||||
expect(tree.sequence[0].condition).toEqual({
|
||||
executed: true,
|
||||
passed: true,
|
||||
failed: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it.each<Action>([
|
||||
{ action: "light.turn_on", enabled: false },
|
||||
{ stop: "Done", enabled: false },
|
||||
|
||||
@@ -297,6 +297,7 @@ it("browses the way the link says", async () => {
|
||||
|
||||
it("browses the way the link says, also when the tab was open before", async () => {
|
||||
// The router keeps the page of a tab, and hangs it back in when it is opened
|
||||
window.history.replaceState(null, "", "/marketplace/browse");
|
||||
const dashboard = await openDashboard();
|
||||
dashboard.remove();
|
||||
window.history.replaceState(null, "", "/marketplace/browse?type=theme");
|
||||
|
||||
Reference in new issue
Block a user