mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-04 21:52:59 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 806d72a851 |
@@ -0,0 +1,46 @@
|
||||
// Gulp transform that brotli-compresses files, several at a time.
|
||||
//
|
||||
// Drop-in replacement for gulp-brotli. zlib already does the work off the main
|
||||
// thread, but that plugin wraps it in through2, which waits for each file
|
||||
// before starting the next, so only one compression is ever in flight. The
|
||||
// compressed bytes are unchanged; only how many run at once differs.
|
||||
//
|
||||
// The real ceiling is libuv's threadpool, which zlib runs on. It sizes itself
|
||||
// from UV_THREADPOOL_SIZE before any JavaScript runs, so it can only be raised
|
||||
// from the environment, never from inside the build.
|
||||
|
||||
import { availableParallelism } from "node:os";
|
||||
import { buffer as readStream } from "node:stream/consumers";
|
||||
import { promisify } from "node:util";
|
||||
import { brotliCompress } from "node:zlib";
|
||||
import { ParallelTransform } from "./parallel-transform.mjs";
|
||||
|
||||
const EXTENSION = ".br";
|
||||
|
||||
const compress = promisify(brotliCompress);
|
||||
|
||||
/**
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.skipLarger] Drop files that compression grows.
|
||||
* @param {object} [options.params] Brotli parameters, passed to zlib as-is.
|
||||
*/
|
||||
export default ({ skipLarger = false, params } = {}) =>
|
||||
new ParallelTransform(availableParallelism(), async (file) => {
|
||||
if (file.isNull()) {
|
||||
return file;
|
||||
}
|
||||
if (file.isStream()) {
|
||||
file.contents = await readStream(file.contents);
|
||||
}
|
||||
|
||||
const compressed = await compress(file.contents, { params });
|
||||
if (skipLarger && compressed.length >= file.contents.length) {
|
||||
// Dropped rather than passed through, as gulp-brotli did: the
|
||||
// uncompressed file is already in the output directory.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
file.contents = compressed;
|
||||
file.path += EXTENSION;
|
||||
return file;
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { constants } from "node:zlib";
|
||||
import gulp from "gulp";
|
||||
import brotli from "gulp-brotli";
|
||||
import brotli from "../brotli.mjs";
|
||||
import paths from "../paths.cjs";
|
||||
import zopfli from "../zopfli.mjs";
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Object-mode transform that keeps several files in flight at once.
|
||||
//
|
||||
// through2 and node's Transform both wait for the previous callback before
|
||||
// handling the next file, which serialises asynchronous work down to one file
|
||||
// at a time. This keeps `limit` files in flight and applies backpressure beyond
|
||||
// that. Files are emitted in completion order rather than input order.
|
||||
|
||||
import { Transform } from "node:stream";
|
||||
|
||||
export class ParallelTransform extends Transform {
|
||||
#limit;
|
||||
|
||||
#handle;
|
||||
|
||||
#inFlight = 0;
|
||||
|
||||
#resume;
|
||||
|
||||
#finish;
|
||||
|
||||
constructor(limit, handle) {
|
||||
super({ objectMode: true, highWaterMark: limit });
|
||||
this.#limit = limit;
|
||||
this.#handle = handle;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- node's Transform API
|
||||
_transform(file, _encoding, callback) {
|
||||
this.#inFlight += 1;
|
||||
this.#handle(file)
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
this.push(result);
|
||||
}
|
||||
})
|
||||
.catch((error) => this.destroy(error))
|
||||
.finally(() => {
|
||||
this.#inFlight -= 1;
|
||||
const resume = this.#resume;
|
||||
this.#resume = undefined;
|
||||
resume?.();
|
||||
if (this.#inFlight === 0) {
|
||||
const finish = this.#finish;
|
||||
this.#finish = undefined;
|
||||
finish?.();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.#inFlight < this.#limit) {
|
||||
callback();
|
||||
} else {
|
||||
this.#resume = callback;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- node's Transform API
|
||||
_flush(callback) {
|
||||
if (this.#inFlight === 0) {
|
||||
callback();
|
||||
} else {
|
||||
this.#finish = callback;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@
|
||||
// instance per worker parallelises it; the output bytes are unchanged.
|
||||
|
||||
import { availableParallelism } from "node:os";
|
||||
import { Transform } from "node:stream";
|
||||
import { buffer as readStream } from "node:stream/consumers";
|
||||
import { Worker } from "node:worker_threads";
|
||||
import { ParallelTransform } from "./parallel-transform.mjs";
|
||||
|
||||
const WORKER_URL = new URL("./zopfli-worker.mjs", import.meta.url);
|
||||
const EXTENSION = ".gz";
|
||||
@@ -112,65 +112,6 @@ const sharedPool = () => {
|
||||
return pool;
|
||||
};
|
||||
|
||||
// through2 and node's Transform both wait for the previous callback before
|
||||
// handling the next file, which would serialise the pool down to one worker.
|
||||
// This keeps `limit` files in flight and applies backpressure beyond that.
|
||||
class ParallelTransform extends Transform {
|
||||
#limit;
|
||||
|
||||
#handle;
|
||||
|
||||
#inFlight = 0;
|
||||
|
||||
#resume;
|
||||
|
||||
#finish;
|
||||
|
||||
constructor(limit, handle) {
|
||||
super({ objectMode: true, highWaterMark: limit });
|
||||
this.#limit = limit;
|
||||
this.#handle = handle;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- node's Transform API
|
||||
_transform(file, _encoding, callback) {
|
||||
this.#inFlight += 1;
|
||||
this.#handle(file)
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
this.push(result);
|
||||
}
|
||||
})
|
||||
.catch((error) => this.destroy(error))
|
||||
.finally(() => {
|
||||
this.#inFlight -= 1;
|
||||
const resume = this.#resume;
|
||||
this.#resume = undefined;
|
||||
resume?.();
|
||||
if (this.#inFlight === 0) {
|
||||
const finish = this.#finish;
|
||||
this.#finish = undefined;
|
||||
finish?.();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.#inFlight < this.#limit) {
|
||||
callback();
|
||||
} else {
|
||||
this.#resume = callback;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- node's Transform API
|
||||
_flush(callback) {
|
||||
if (this.#inFlight === 0) {
|
||||
callback();
|
||||
} else {
|
||||
this.#finish = callback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.threshold] Skip files smaller than this many bytes.
|
||||
|
||||
+1
-29
@@ -111,17 +111,7 @@ export default tseslint.config(
|
||||
"no-bitwise": "error",
|
||||
"no-console": "error",
|
||||
"no-restricted-globals": [2, "event"],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
"LabeledStatement",
|
||||
"WithStatement",
|
||||
{
|
||||
selector:
|
||||
"CallExpression[callee.property.name=/^(push|replace)State$/]",
|
||||
message:
|
||||
"Use navigate(), updateHistoryState() or replaceCurrentUrl() from common/navigate. History entries carry the app's own bookkeeping, which a raw pushState/replaceState drops.",
|
||||
},
|
||||
],
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
"wc/no-self-class": "off",
|
||||
|
||||
// import-x rules
|
||||
@@ -232,24 +222,6 @@ export default tseslint.config(
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// These own history entries themselves: the navigation helpers, the dialog
|
||||
// stack, the boot paths that run before the app has any state to keep, and
|
||||
// the tests that fabricate entries to simulate a document load.
|
||||
files: [
|
||||
"src/common/navigate.ts",
|
||||
"src/dialogs/make-dialog-manager.ts",
|
||||
"src/state/url-sync-mixin.ts",
|
||||
"src/panels/config/automation/add-automation-element-dialog.ts",
|
||||
"src/entrypoints/core.ts",
|
||||
"src/onboarding/**/*.ts",
|
||||
"cast/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
],
|
||||
rules: {
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/util/recorder-worklet.js"],
|
||||
languageOptions: {
|
||||
|
||||
@@ -188,7 +188,6 @@
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.8.0",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { ReactiveElement } from "lit";
|
||||
import { updateHistoryState } from "../navigate";
|
||||
import { throttle } from "../util/throttle";
|
||||
|
||||
const throttleReplaceState = throttle((value) => {
|
||||
updateHistoryState({ scrollPosition: value });
|
||||
history.replaceState({ scrollPosition: value }, "");
|
||||
}, 300);
|
||||
|
||||
export function restoreScroll(selector: string) {
|
||||
|
||||
+39
-59
@@ -1,7 +1,6 @@
|
||||
import { closeAllDialogs } from "../dialogs/make-dialog-manager";
|
||||
import { fireEvent } from "./dom/fire_event";
|
||||
import { mainWindow } from "./dom/get_main_window";
|
||||
import { currentPath } from "./url/current-path";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -18,26 +17,6 @@ export interface NavigateOptions {
|
||||
// max time to wait for dialogs to close before navigating
|
||||
const DIALOG_WAIT_TIMEOUT = 500;
|
||||
|
||||
/**
|
||||
* Merge into the current history entry's state, keeping what is already there.
|
||||
* Entries carry the app's own bookkeeping (`from`, `root`, dialog state), so
|
||||
* they must never be replaced wholesale.
|
||||
*/
|
||||
export const updateHistoryState = (patch: Record<string, unknown>) => {
|
||||
mainWindow.history.replaceState(
|
||||
{ ...mainWindow.history.state, ...patch },
|
||||
""
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrite the URL of the current history entry without navigating and without
|
||||
* touching its state. For query parameter cleanup.
|
||||
*/
|
||||
export const replaceCurrentUrl = (url: string) => {
|
||||
mainWindow.history.replaceState(mainWindow.history.state, "", url);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stash a destination URL in the current history entry's state. If the page
|
||||
* is refreshed while a dialog is open, urlSyncMixin will navigate to this URL
|
||||
@@ -45,7 +24,10 @@ export const replaceCurrentUrl = (url: string) => {
|
||||
* The current URL is not changed.
|
||||
*/
|
||||
export const setRefreshUrl = (path: string) => {
|
||||
updateHistoryState({ refreshUrl: path });
|
||||
mainWindow.history.replaceState(
|
||||
{ ...mainWindow.history.state, refreshUrl: path },
|
||||
""
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -81,32 +63,37 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||
}
|
||||
const replace = options?.replace || false;
|
||||
|
||||
if (__DEMO__ && !path.includes("#")) {
|
||||
// The demo routes with the hash instead of the pathname. Resolve the
|
||||
// path like the browser would do for pushState, and keep the query
|
||||
// parameters in the URL query instead of inside the hash.
|
||||
const url = new URL(
|
||||
path,
|
||||
`${mainWindow.location.origin}${mainWindow.location.hash.substring(1)}`
|
||||
);
|
||||
path = `${mainWindow.location.pathname}${url.search}#${url.pathname}`;
|
||||
}
|
||||
|
||||
const { history } = mainWindow;
|
||||
|
||||
if (replace) {
|
||||
// A replaced entry keeps its predecessor, so it keeps `from`.
|
||||
const { root, from } = history.state ?? {};
|
||||
const state = root ? { root: true } : (options?.data ?? null);
|
||||
history.replaceState(
|
||||
from === undefined ? state : { ...state, from },
|
||||
if (__DEMO__) {
|
||||
if (!path.includes("#")) {
|
||||
// The demo routes with the hash instead of the pathname. Resolve the
|
||||
// path like the browser would do for pushState, and keep the query
|
||||
// parameters in the URL query instead of inside the hash.
|
||||
const url = new URL(
|
||||
path,
|
||||
`${mainWindow.location.origin}${mainWindow.location.hash.substring(1)}`
|
||||
);
|
||||
path = `${mainWindow.location.pathname}${url.search}#${url.pathname}`;
|
||||
}
|
||||
if (replace) {
|
||||
mainWindow.history.replaceState(
|
||||
mainWindow.history.state?.root
|
||||
? { root: true }
|
||||
: (options?.data ?? null),
|
||||
"",
|
||||
path
|
||||
);
|
||||
} else {
|
||||
mainWindow.history.pushState(options?.data ?? null, "", path);
|
||||
}
|
||||
} else if (replace) {
|
||||
mainWindow.history.replaceState(
|
||||
mainWindow.history.state?.root ? { root: true } : (options?.data ?? null),
|
||||
"",
|
||||
path
|
||||
);
|
||||
} else {
|
||||
history.pushState({ ...options?.data, from: currentPath() }, "", path);
|
||||
mainWindow.history.pushState(options?.data ?? null, "", path);
|
||||
}
|
||||
|
||||
fireEvent(mainWindow, "location-changed", {
|
||||
replace,
|
||||
});
|
||||
@@ -114,17 +101,8 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the previous history entry is a page this app navigated away from.
|
||||
* `history.length` cannot answer this: a login redirect goes through
|
||||
* `location.assign`, which leaves /auth/authorize right behind the requested
|
||||
* page, and going back there would bounce the user out of the app.
|
||||
*/
|
||||
export const canGoBack = (): boolean =>
|
||||
mainWindow.history.state?.from !== undefined;
|
||||
|
||||
/**
|
||||
* Navigate back to the page we came from, falling back to a path when the
|
||||
* previous entry is not ours (deep link, login redirect, fresh tab).
|
||||
* Navigate back in history, with fallback to a default path if no history exists.
|
||||
* This prevents a user from getting stuck when they navigate directly to a page with no history.
|
||||
*/
|
||||
export const goBack = async (fallbackPath?: string): Promise<void> => {
|
||||
const canProceed = await ensureDialogsClosed(Date.now());
|
||||
@@ -132,12 +110,14 @@ export const goBack = async (fallbackPath?: string): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read after closing dialogs: their history entries are popped by then, so
|
||||
// this is the state of the page entry.
|
||||
if (canGoBack()) {
|
||||
mainWindow.history.back();
|
||||
// Check if we have history to go back to
|
||||
const { history } = mainWindow;
|
||||
if (history.length > 1) {
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(fallbackPath || "/", { replace: true });
|
||||
// No history available, navigate to fallback path
|
||||
const fallback = fallbackPath || "/";
|
||||
navigate(fallback, { replace: true });
|
||||
};
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { mainWindow } from "../dom/get_main_window";
|
||||
|
||||
/**
|
||||
* The path of the page currently shown by the app. The demo routes with the
|
||||
* hash instead of the pathname, see navigate().
|
||||
*/
|
||||
export const currentPath = (): string =>
|
||||
__DEMO__
|
||||
? mainWindow.location.hash.substring(1)
|
||||
: mainWindow.location.pathname;
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
getEntityEntryContext,
|
||||
} from "../../common/entity/context/get_entity_context";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
|
||||
import { navigate, updateHistoryState } from "../../common/navigate";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { withViewTransition } from "../../common/util/view-transition";
|
||||
@@ -268,12 +268,16 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
}
|
||||
|
||||
private _setView(view: MoreInfoView) {
|
||||
updateHistoryState({
|
||||
dialogParams: {
|
||||
...history.state?.dialogParams,
|
||||
view,
|
||||
history.replaceState(
|
||||
{
|
||||
...history.state,
|
||||
dialogParams: {
|
||||
...history.state?.dialogParams,
|
||||
view,
|
||||
},
|
||||
},
|
||||
});
|
||||
""
|
||||
);
|
||||
this._currView = view;
|
||||
}
|
||||
|
||||
|
||||
+14
-19
@@ -4,7 +4,6 @@ import { customElement, eventOptions, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { restoreScroll } from "../common/decorators/restore-scroll";
|
||||
import type { HASSDomTargetEvent } from "../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import { goBack } from "../common/navigate";
|
||||
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
|
||||
import "../components/ha-icon-button-arrow-prev";
|
||||
@@ -38,14 +37,19 @@ class HassSubpage extends LitElement {
|
||||
<div class="toolbar ${classMap({ narrow: this.narrow })}">
|
||||
<div class="toolbar-content">
|
||||
${
|
||||
this.mainPage || (!backPath && history.state?.root)
|
||||
this.mainPage || history.state?.root
|
||||
? html`<ha-menu-button></ha-menu-button>`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.href=${backPath}
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: backPath
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
href=${backPath}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
}
|
||||
|
||||
<div class="main-title">
|
||||
@@ -75,21 +79,12 @@ class HassSubpage extends LitElement {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
|
||||
private _backTapped(ev: MouseEvent): void {
|
||||
const backPath = sanitizeNavigationPath(this.backPath);
|
||||
|
||||
// Middle click, ctrl and cmd open the parent in a new tab, let the anchor
|
||||
// handle those.
|
||||
if (backPath && !isNavigationClick(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
private _backTapped(): void {
|
||||
if (this.backCallback) {
|
||||
this.backCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
goBack(backPath);
|
||||
goBack();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -175,12 +175,17 @@ export class HassTabsSubpage extends LitElement {
|
||||
${
|
||||
this.mainPage || (!backPath && history.state?.root)
|
||||
? html`<ha-menu-button></ha-menu-button>`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.href=${backPath}
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: backPath
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.href=${backPath}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
}
|
||||
${
|
||||
this._narrow || !this.showTabs
|
||||
@@ -241,21 +246,12 @@ export class HassTabsSubpage extends LitElement {
|
||||
this._content.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
private _backTapped(ev: MouseEvent): void {
|
||||
const backPath = sanitizeNavigationPath(this.backPath);
|
||||
|
||||
// Middle click, ctrl and cmd open the parent in a new tab, let the anchor
|
||||
// handle those.
|
||||
if (backPath && !isNavigationClick(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
private _backTapped(): void {
|
||||
if (this.backCallback) {
|
||||
this.backCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
goBack(backPath);
|
||||
goBack();
|
||||
}
|
||||
|
||||
private _isActiveTabPath(tabPath: string, currentPath: string): boolean {
|
||||
|
||||
@@ -644,7 +644,6 @@ class HaConfigAreaPage extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/areas/dashboard"
|
||||
.header=${html`${
|
||||
area.icon
|
||||
? html`<ha-icon
|
||||
|
||||
@@ -86,6 +86,8 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
|
||||
@state() private _hierarchy?: AreasFloorHierarchy;
|
||||
|
||||
private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
private _blockHierarchyUpdate = false;
|
||||
|
||||
private _blockHierarchyUpdateTimeout?: number;
|
||||
@@ -166,7 +168,9 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.isWide=${this.isWide}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.tabs=${configSections.areas}
|
||||
.route=${this.route}
|
||||
has-fab
|
||||
|
||||
@@ -443,7 +443,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
id="entity_id"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate, replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
@@ -110,7 +110,6 @@ export class HaAutomationTrace extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/automation/dashboard"
|
||||
.header=${title}
|
||||
.scrollable=${this.narrow}
|
||||
>
|
||||
@@ -453,7 +452,11 @@ export class HaAutomationTrace extends LitElement {
|
||||
if (runId) {
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.delete("run_id");
|
||||
replaceCurrentUrl(`${location.pathname}?${params.toString()}`);
|
||||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`${location.pathname}?${params.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
await showAlertDialog(this, {
|
||||
|
||||
@@ -11,7 +11,6 @@ import { property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
import {
|
||||
extractSearchParam,
|
||||
@@ -172,7 +171,11 @@ export const ManualEditorMixin = <TConfig>(
|
||||
}
|
||||
|
||||
protected clearParam(param: string) {
|
||||
replaceCurrentUrl(constructUrlCurrentPath(removeSearchParam(param)));
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(removeSearchParam(param))
|
||||
);
|
||||
}
|
||||
|
||||
protected async openSidebar(ev: CustomEvent<SidebarConfig>) {
|
||||
|
||||
@@ -74,6 +74,8 @@ class HaConfigBackupOverview extends LitElement {
|
||||
|
||||
@state() private _config?: BackupConfig;
|
||||
|
||||
private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("config") && !this._config) {
|
||||
@@ -204,7 +206,9 @@ class HaConfigBackupOverview extends LitElement {
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config/system"
|
||||
}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.backup.overview.header")}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-alert";
|
||||
@@ -119,7 +118,7 @@ class HaConfigBackupSettings extends LitElement {
|
||||
}
|
||||
|
||||
private _clearHash() {
|
||||
replaceCurrentUrl(window.location.pathname);
|
||||
history.replaceState(null, "", window.location.pathname);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
||||
@@ -21,7 +21,6 @@ export class CloudForgotPassword extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.cloud.forgot_password.title"
|
||||
)}
|
||||
|
||||
@@ -45,7 +45,6 @@ export class CloudLoginPanel extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
header="Home Assistant Cloud"
|
||||
>
|
||||
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
|
||||
|
||||
@@ -38,7 +38,6 @@ export class CloudRegister extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.header=${this.hass.localize("ui.panel.config.cloud.register.title")}
|
||||
>
|
||||
<div class="content">
|
||||
|
||||
@@ -66,6 +66,8 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _showSkipped = false;
|
||||
|
||||
@state() private _supervisorInfo?: HassioSupervisorInfo;
|
||||
@@ -153,7 +155,9 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config/system"
|
||||
}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.updates.caption")}
|
||||
|
||||
@@ -986,7 +986,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
return html`<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/devices/dashboard"
|
||||
.header=${deviceName}
|
||||
>
|
||||
<ha-tooltip for="edit-settings-button" slot="toolbar-icon">
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { navigate, updateHistoryState } from "../../../common/navigate";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
@@ -778,7 +778,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.tabs=${configSections.devices}
|
||||
.route=${this.route}
|
||||
.searchLabel=${this.hass.localize(
|
||||
@@ -1041,7 +1043,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
updateHistoryState({ filter: this._filter });
|
||||
history.replaceState({ filter: this._filter }, "");
|
||||
}
|
||||
|
||||
private _addDevice() {
|
||||
|
||||
@@ -78,6 +78,8 @@ class HaConfigEnergy extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _info?: EnergyInfo;
|
||||
|
||||
@state() private _preferences?: EnergyPreferences;
|
||||
@@ -124,7 +126,11 @@ class HaConfigEnergy extends LitElement {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
back-path="/config/lovelace/dashboards"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config/lovelace/dashboards"
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${TABS}
|
||||
>
|
||||
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { updateHistoryState } from "../../../common/navigate";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
@@ -811,7 +810,9 @@ export class HaConfigEntities extends LitElement {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.columns=${this._columns(this.hass.localize, filteredEntities)}
|
||||
@@ -1245,7 +1246,7 @@ export class HaConfigEntities extends LitElement {
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
updateHistoryState({ filter: this._filter });
|
||||
history.replaceState({ filter: this._filter }, "");
|
||||
}
|
||||
|
||||
private _handleSelectionChanged(
|
||||
|
||||
@@ -644,7 +644,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.searchLabel=${this.hass.localize(
|
||||
|
||||
@@ -373,11 +373,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
: this._manifest?.documentation;
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/integrations/dashboard"
|
||||
>
|
||||
<hass-subpage .hass=${this.hass} .narrow=${this.narrow}>
|
||||
${
|
||||
documentationLink
|
||||
? html`
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { navigate, updateHistoryState } from "../../../common/navigate";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
@@ -158,6 +158,8 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
window.location.hash.substring(1)
|
||||
);
|
||||
|
||||
@state() private _searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _filter: string = history.state?.filter || "";
|
||||
|
||||
@state() private _logInfos?: Record<string, IntegrationLogInfo>;
|
||||
@@ -501,7 +503,9 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParams.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
has-fab
|
||||
@@ -858,7 +862,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
this._filter = (ev.target as HaInputSearch).value ?? "";
|
||||
updateHistoryState({ filter: this._filter });
|
||||
history.replaceState({ filter: this._filter }, "");
|
||||
}
|
||||
|
||||
private async _highlightEntry() {
|
||||
|
||||
@@ -95,7 +95,6 @@ export class DHCPConfigPanel extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config/integrations/integration/dhcp"
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._dataWithIds(this._data)}
|
||||
.noDataText=${this.hass.localize(
|
||||
|
||||
@@ -61,11 +61,7 @@ export class MQTTConfigPanel extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
back-path="/config/integrations/integration/mqtt"
|
||||
>
|
||||
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
|
||||
<div class="content">
|
||||
<ha-card
|
||||
.header=${this.hass.localize("ui.panel.config.mqtt.settings_title")}
|
||||
|
||||
@@ -99,7 +99,6 @@ export class SSDPConfigPanel extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config/integrations/integration/ssdp"
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
|
||||
@@ -106,7 +106,6 @@ export class ZeroconfConfigPanel extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config/integrations/integration/zeroconf"
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
|
||||
@@ -76,7 +76,6 @@ class ZHAAddDevicesPage extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/zha/dashboard"
|
||||
.header=${this.hass.localize("ui.panel.config.zha.add_device")}
|
||||
>
|
||||
<ha-button
|
||||
|
||||
-1
@@ -55,7 +55,6 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/zha/dashboard"
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.zha.visualization.header"
|
||||
)}
|
||||
|
||||
+2
-12
@@ -34,12 +34,7 @@ class ZWaveJSConfigEntryPicker extends LitElement {
|
||||
|
||||
if (this._configEntries.length === 0) {
|
||||
return html`
|
||||
<hass-subpage
|
||||
header="Z-Wave"
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
back-path="/config/integrations/integration/zwave_js"
|
||||
>
|
||||
<hass-subpage header="Z-Wave" .narrow=${this.narrow} .hass=${this.hass}>
|
||||
<div class="content">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
@@ -56,12 +51,7 @@ class ZWaveJSConfigEntryPicker extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
header="Z-Wave"
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
back-path="/config/integrations/integration/zwave_js"
|
||||
>
|
||||
<hass-subpage header="Z-Wave" .narrow=${this.narrow} .hass=${this.hass}>
|
||||
<div class="content">
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
|
||||
@@ -175,7 +175,6 @@ export class HaConfigLovelaceResources extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config/lovelace/dashboards"
|
||||
.tabs=${lovelaceResourcesTabs}
|
||||
.columns=${this._columns(this.hass.language, this.hass.localize)}
|
||||
.data=${this._resources}
|
||||
|
||||
@@ -32,6 +32,8 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _repairsIssues: RepairsIssue[] = [];
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _showIgnored = false;
|
||||
|
||||
private _getFilteredIssues = memoizeOne(
|
||||
@@ -75,7 +77,9 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config/system"
|
||||
}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.repairs.caption")}
|
||||
|
||||
@@ -459,7 +459,9 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.searchLabel=${this.hass.localize(
|
||||
|
||||
@@ -430,7 +430,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.searchLabel=${this.hass.localize(
|
||||
|
||||
@@ -14,7 +14,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate, replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
@@ -106,7 +106,6 @@ export class HaScriptTrace extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/script/dashboard"
|
||||
.header=${title}
|
||||
.scrollable=${this.narrow}
|
||||
>
|
||||
@@ -434,7 +433,11 @@ export class HaScriptTrace extends LitElement {
|
||||
if (runId) {
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.delete("run_id");
|
||||
replaceCurrentUrl(`${location.pathname}?${params.toString()}`);
|
||||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`${location.pathname}?${params.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
await showAlertDialog(this, {
|
||||
|
||||
@@ -128,7 +128,6 @@ class AssistDevicesPage extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/voice-assistants/assistants"
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.pipeline.devices.title"
|
||||
)}
|
||||
|
||||
@@ -51,7 +51,6 @@ export class AssistPipelineDebug extends LitElement {
|
||||
return html`<hass-subpage
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
back-path="/config/voice-assistants/debug"
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.debug.header"
|
||||
)}
|
||||
|
||||
@@ -60,7 +60,6 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
<hass-subpage
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
back-path="/config/voice-assistants/assistants"
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.debug.pipeline.header"
|
||||
)}
|
||||
|
||||
@@ -29,6 +29,8 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
@@ -37,7 +39,9 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${voiceAssistantTabs}
|
||||
>
|
||||
|
||||
@@ -492,7 +492,9 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${voiceAssistantTabs}
|
||||
.columns=${this._columns(
|
||||
|
||||
@@ -57,6 +57,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _storageItems?: Zone[];
|
||||
|
||||
@state() private _stateItems?: HassEntity[];
|
||||
@@ -246,7 +248,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.tabs=${configSections.areas}
|
||||
has-fab
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { navigate, replaceCurrentUrl } from "../../common/navigate";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import {
|
||||
@@ -509,7 +509,9 @@ export class LovelacePanel extends LitElement {
|
||||
};
|
||||
|
||||
if ("editMode" in props) {
|
||||
replaceCurrentUrl(
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(
|
||||
props.editMode
|
||||
? addSearchParam({ edit: "1" })
|
||||
|
||||
@@ -28,7 +28,7 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../../common/dom/is-navigation-click";
|
||||
import { goBack, navigate, replaceCurrentUrl } from "../../common/navigate";
|
||||
import { goBack, navigate } from "../../common/navigate";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import { sanitizeNavigationPath } from "../../common/url/sanitize-navigation-path";
|
||||
@@ -677,7 +677,11 @@ class HUIRoot extends LitElement {
|
||||
);
|
||||
|
||||
private _clearParam(param: string) {
|
||||
replaceCurrentUrl(constructUrlCurrentPath(removeSearchParam(param)));
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(removeSearchParam(param))
|
||||
);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
@@ -890,10 +894,22 @@ class HUIRoot extends LitElement {
|
||||
|
||||
private _goBack(): void {
|
||||
const views = this.lovelace?.config.views ?? [];
|
||||
// Falling back to the first view only makes sense when it is a real view.
|
||||
const fallbackPath = views[0]?.subview ? undefined : this.route?.prefix;
|
||||
const curViewConfig =
|
||||
typeof this._curView === "number" ? views[this._curView] : undefined;
|
||||
|
||||
goBack(this._backPath ?? fallbackPath);
|
||||
const backPath = sanitizeNavigationPath(
|
||||
curViewConfig?.back_path ?? this.backPath
|
||||
);
|
||||
|
||||
if (backPath) {
|
||||
navigate(backPath, { replace: true });
|
||||
} else if (history.length > 1) {
|
||||
goBack();
|
||||
} else if (!views[0].subview) {
|
||||
navigate(this.route!.prefix, { replace: true });
|
||||
} else {
|
||||
navigate("/");
|
||||
}
|
||||
}
|
||||
|
||||
private _handleBackClick(ev: MouseEvent): void {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { clamp } from "../../../common/number/clamp";
|
||||
import { updateHistoryState } from "../../../common/navigate";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-sortable";
|
||||
@@ -495,7 +494,10 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
this._sidebarTabActive = !this._sidebarTabActive;
|
||||
|
||||
// Add sidebar state to history
|
||||
updateHistoryState({ sidebar: this._sidebarTabActive });
|
||||
window.history.replaceState(
|
||||
{ ...window.history.state, sidebar: this._sidebarTabActive },
|
||||
""
|
||||
);
|
||||
|
||||
// Restore scroll position after view updates
|
||||
this.updateComplete.then(() => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { replaceCurrentUrl } from "../../common/navigate";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-card";
|
||||
@@ -91,7 +90,7 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
}
|
||||
|
||||
private _clearHash() {
|
||||
replaceCurrentUrl(window.location.pathname);
|
||||
history.replaceState(null, "", window.location.pathname);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import type { ReactiveElement, PropertyValues } from "lit";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { updateHistoryState } from "../common/navigate";
|
||||
import { closeLastDialog } from "../dialogs/make-dialog-manager";
|
||||
import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
|
||||
import type { Constructor } from "../types";
|
||||
@@ -21,7 +20,10 @@ export const urlSyncMixin = <
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (mainWindow.history.length === 1) {
|
||||
updateHistoryState({ root: true });
|
||||
mainWindow.history.replaceState(
|
||||
{ ...mainWindow.history.state, root: true },
|
||||
""
|
||||
);
|
||||
}
|
||||
mainWindow.addEventListener("popstate", this._popstateChangeListener);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { Buffer } from "node:buffer";
|
||||
import { Readable } from "node:stream";
|
||||
import { promisify } from "node:util";
|
||||
import { brotliCompress, constants } from "node:zlib";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import brotli from "../../build-scripts/brotli.mjs";
|
||||
import { file, filler, run as runStream } from "./vinyl-stub.js";
|
||||
|
||||
// What build-scripts/gulp/compress.js asks for.
|
||||
const PARAMS = {
|
||||
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
|
||||
};
|
||||
|
||||
const run = (files, options = { skipLarger: true, params: PARAMS }) =>
|
||||
runStream(brotli(options), files);
|
||||
|
||||
const compressInProcess = promisify(brotliCompress);
|
||||
|
||||
describe("brotli", () => {
|
||||
it("appends .br and matches zlib byte for byte", async () => {
|
||||
const contents = filler(4096);
|
||||
const [result] = await run([file("/out/app.js", contents)]);
|
||||
|
||||
expect(result.path).toBe("/out/app.js.br");
|
||||
expect(result.contents).toEqual(
|
||||
await compressInProcess(contents, { params: PARAMS })
|
||||
);
|
||||
});
|
||||
|
||||
it("returns contents as a Buffer, which vinyl requires", async () => {
|
||||
const [result] = await run([file("/out/app.js", filler(1024))]);
|
||||
|
||||
expect(Buffer.isBuffer(result.contents)).toBe(true);
|
||||
});
|
||||
|
||||
it("appends to the path rather than replacing the extension", async () => {
|
||||
const [result] = await run([file("/out/nested/chunk.min.js", filler(500))]);
|
||||
|
||||
expect(result.path).toBe("/out/nested/chunk.min.js.br");
|
||||
});
|
||||
|
||||
it("drops files that compression grows when skipLarger is set", async () => {
|
||||
expect(await run([file("/out/tiny.js", filler(1))])).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps files that compression grows when skipLarger is not set", async () => {
|
||||
const [result] = await run([file("/out/tiny.js", filler(1))], {
|
||||
params: PARAMS,
|
||||
});
|
||||
|
||||
expect(result.path).toBe("/out/tiny.js.br");
|
||||
});
|
||||
|
||||
it("passes null files through", async () => {
|
||||
const [result] = await run([file("/out/adirectory", null)]);
|
||||
|
||||
expect(result.path).toBe("/out/adirectory");
|
||||
expect(result.contents).toBeNull();
|
||||
});
|
||||
|
||||
it("buffers stream-mode contents", async () => {
|
||||
const contents = filler(600);
|
||||
const [result] = await run([
|
||||
file("/out/streamed.js", Readable.from([contents])),
|
||||
]);
|
||||
|
||||
expect(result.path).toBe("/out/streamed.js.br");
|
||||
expect(result.contents).toEqual(
|
||||
await compressInProcess(contents, { params: PARAMS })
|
||||
);
|
||||
});
|
||||
|
||||
it("handles more files than it keeps in flight", async () => {
|
||||
const count = 40;
|
||||
const files = Array.from({ length: count }, (_, index) =>
|
||||
file(`/out/chunk${index}.js`, filler(200 + index))
|
||||
);
|
||||
|
||||
const results = await run(files);
|
||||
|
||||
expect(results).toHaveLength(count);
|
||||
expect(new Set(results.map((result) => result.path)).size).toBe(count);
|
||||
await Promise.all(
|
||||
results.map(async (result) => {
|
||||
const index = Number(result.path.match(/chunk(\d+)/)[1]);
|
||||
expect(result.contents).toEqual(
|
||||
await compressInProcess(filler(200 + index), { params: PARAMS })
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { setImmediate } from "node:timers/promises";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ParallelTransform } from "../../build-scripts/parallel-transform.mjs";
|
||||
import { run } from "./vinyl-stub.js";
|
||||
|
||||
const defer = () => {
|
||||
const handle = {};
|
||||
handle.promise = new Promise((resolve, reject) => {
|
||||
handle.resolve = resolve;
|
||||
handle.reject = reject;
|
||||
});
|
||||
return handle;
|
||||
};
|
||||
|
||||
describe("ParallelTransform", () => {
|
||||
it("keeps `limit` handlers in flight and no more", async () => {
|
||||
const pending = [];
|
||||
let active = 0;
|
||||
let peak = 0;
|
||||
|
||||
const stream = new ParallelTransform(3, () => {
|
||||
active += 1;
|
||||
peak = Math.max(peak, active);
|
||||
const handle = defer();
|
||||
pending.push(handle);
|
||||
return handle.promise.then((result) => {
|
||||
active -= 1;
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
const items = Array.from({ length: 10 }, (_, index) => `item${index}`);
|
||||
const done = run(stream, items);
|
||||
|
||||
await setImmediate();
|
||||
expect(pending).toHaveLength(3);
|
||||
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
pending[index].resolve(items[index]);
|
||||
// eslint-disable-next-line no-await-in-loop -- releasing one job at a time is the point
|
||||
await setImmediate();
|
||||
}
|
||||
|
||||
expect(await done).toEqual(items);
|
||||
expect(peak).toBe(3);
|
||||
});
|
||||
|
||||
it("emits in completion order rather than input order", async () => {
|
||||
const pending = new Map();
|
||||
const stream = new ParallelTransform(3, (item) => {
|
||||
const handle = defer();
|
||||
pending.set(item, handle);
|
||||
return handle.promise;
|
||||
});
|
||||
|
||||
const done = run(stream, ["a", "b", "c"]);
|
||||
await setImmediate();
|
||||
|
||||
["c", "a", "b"].forEach((item) => pending.get(item).resolve(item));
|
||||
|
||||
expect(await done).toEqual(["c", "a", "b"]);
|
||||
});
|
||||
|
||||
it("drops results the handler resolves to nothing for", async () => {
|
||||
const stream = new ParallelTransform(2, async (item) =>
|
||||
item === "skip" ? undefined : item
|
||||
);
|
||||
|
||||
expect(await run(stream, ["a", "skip", "b"])).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("fails the stream when a handler rejects", async () => {
|
||||
const stream = new ParallelTransform(2, async (item) => {
|
||||
if (item === "bad") {
|
||||
throw new Error("boom");
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
await expect(run(stream, ["a", "bad", "b"])).rejects.toThrow("boom");
|
||||
});
|
||||
|
||||
it("does not finish until in-flight work completes", async () => {
|
||||
const handle = defer();
|
||||
let ended = false;
|
||||
|
||||
const stream = new ParallelTransform(2, () => handle.promise);
|
||||
const done = run(stream, ["a"]).then((results) => {
|
||||
ended = true;
|
||||
return results;
|
||||
});
|
||||
|
||||
await setImmediate();
|
||||
expect(ended).toBe(false);
|
||||
|
||||
handle.resolve("a");
|
||||
|
||||
expect(await done).toEqual(["a"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
// Stand-in for the vinyl files gulp.src yields in buffer mode.
|
||||
export const file = (path, contents) => ({
|
||||
path,
|
||||
contents,
|
||||
isNull() {
|
||||
return this.contents === null;
|
||||
},
|
||||
isStream() {
|
||||
return this.contents instanceof Readable;
|
||||
},
|
||||
});
|
||||
|
||||
export const filler = (length) => Buffer.alloc(length, "a");
|
||||
|
||||
export const run = (stream, files) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const out = [];
|
||||
stream.on("data", (result) => out.push(result));
|
||||
stream.on("error", reject);
|
||||
stream.on("end", () => resolve(out));
|
||||
files.forEach((entry) => stream.write(entry));
|
||||
stream.end();
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import process from "node:process";
|
||||
import { Readable } from "node:stream";
|
||||
import gfxZopfli from "@gfx/zopfli";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { file, filler, run as runStream } from "./vinyl-stub.js";
|
||||
|
||||
// Keep the pool small; it is created once, on the first factory call.
|
||||
process.env.ZOPFLI_WORKERS = "2";
|
||||
@@ -15,28 +16,8 @@ const { default: zopfli } = await import("../../build-scripts/zopfli.mjs");
|
||||
|
||||
const THRESHOLD = 150;
|
||||
|
||||
// Stand-in for the vinyl files gulp.src yields in buffer mode.
|
||||
const file = (path, contents) => ({
|
||||
path,
|
||||
contents,
|
||||
isNull() {
|
||||
return this.contents === null;
|
||||
},
|
||||
isStream() {
|
||||
return this.contents instanceof Readable;
|
||||
},
|
||||
});
|
||||
|
||||
const run = (files, options = { threshold: THRESHOLD }) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const stream = zopfli(options);
|
||||
const out = [];
|
||||
stream.on("data", (result) => out.push(result));
|
||||
stream.on("error", reject);
|
||||
stream.on("end", () => resolve(out));
|
||||
files.forEach((entry) => stream.write(entry));
|
||||
stream.end();
|
||||
});
|
||||
runStream(zopfli(options), files);
|
||||
|
||||
// What the old in-process plugin did, for byte-for-byte comparison.
|
||||
const gzipInProcess = (contents) =>
|
||||
@@ -46,8 +27,6 @@ const gzipInProcess = (contents) =>
|
||||
);
|
||||
});
|
||||
|
||||
const filler = (length) => Buffer.alloc(length, "a");
|
||||
|
||||
describe("zopfli worker pool", () => {
|
||||
it("appends .gz and matches in-process zopfli byte for byte", async () => {
|
||||
const contents = filler(4096);
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { canGoBack, goBack, navigate } from "../../src/common/navigate";
|
||||
|
||||
// navigate() closes open dialogs before touching history.
|
||||
vi.mock("../../src/dialogs/make-dialog-manager", () => ({
|
||||
closeAllDialogs: vi.fn(async () => true),
|
||||
}));
|
||||
|
||||
// Sets up a raw entry the way a document load does, which is exactly what the
|
||||
// helpers in common/navigate refuse to produce.
|
||||
const setEntry = (path: string, state: unknown = null) => {
|
||||
window.history.replaceState(state, "", path);
|
||||
};
|
||||
|
||||
describe("navigate", () => {
|
||||
beforeEach(() => {
|
||||
setEntry("/config");
|
||||
});
|
||||
|
||||
it("stamps the path we came from on pushed entries", async () => {
|
||||
await navigate("/config/devices/dashboard");
|
||||
|
||||
expect(window.location.pathname).toEqual("/config/devices/dashboard");
|
||||
expect(window.history.state).toMatchObject({ from: "/config" });
|
||||
});
|
||||
|
||||
it("keeps caller data alongside the stamp", async () => {
|
||||
await navigate("/config/areas", { data: { scrollPosition: 42 } });
|
||||
|
||||
expect(window.history.state).toMatchObject({
|
||||
scrollPosition: 42,
|
||||
from: "/config",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the stamp when replacing, the predecessor is unchanged", async () => {
|
||||
await navigate("/config/cloud");
|
||||
await navigate("/config/cloud/account", { replace: true });
|
||||
|
||||
expect(window.location.pathname).toEqual("/config/cloud/account");
|
||||
expect(window.history.state).toMatchObject({ from: "/config" });
|
||||
});
|
||||
|
||||
it("does not stamp an entry the app did not push", () => {
|
||||
expect(window.history.state).toBeNull();
|
||||
expect(canGoBack()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("goBack", () => {
|
||||
beforeEach(() => {
|
||||
setEntry("/config/cloud/remote");
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("goes back when we came from another page in the app", async () => {
|
||||
const back = vi
|
||||
.spyOn(window.history, "back")
|
||||
.mockImplementation(() => undefined);
|
||||
await navigate("/config/cloud/remote");
|
||||
|
||||
await goBack("/config/cloud/account");
|
||||
|
||||
expect(back).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("falls back to the given path when the previous entry is not ours", async () => {
|
||||
// A login redirect lands on the page through location.assign, leaving
|
||||
// /auth/authorize behind: going back there would leave the app.
|
||||
const back = vi
|
||||
.spyOn(window.history, "back")
|
||||
.mockImplementation(() => undefined);
|
||||
|
||||
await goBack("/config/cloud/account");
|
||||
|
||||
expect(back).not.toHaveBeenCalled();
|
||||
expect(window.location.pathname).toEqual("/config/cloud/account");
|
||||
});
|
||||
|
||||
it("falls back to the root when no path is given", async () => {
|
||||
vi.spyOn(window.history, "back").mockImplementation(() => undefined);
|
||||
|
||||
await goBack();
|
||||
|
||||
expect(window.location.pathname).toEqual("/");
|
||||
});
|
||||
});
|
||||
@@ -28,7 +28,7 @@ afterEach(() => {
|
||||
describe("hass-subpage back path", () => {
|
||||
it("links to a path on the current origin", async () => {
|
||||
const backButton = await mount("/config/system");
|
||||
expect(backButton!.href).toEqual("/config/system");
|
||||
expect(backButton!.getAttribute("href")).toEqual("/config/system");
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-script-url
|
||||
@@ -36,7 +36,7 @@ describe("hass-subpage back path", () => {
|
||||
"does not link to %s",
|
||||
async (backPath) => {
|
||||
const backButton = await mount(backPath);
|
||||
expect(backButton!.href).toBeUndefined();
|
||||
expect(backButton!.hasAttribute("href")).toBe(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9714,16 +9714,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gulp-brotli@npm:3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "gulp-brotli@npm:3.0.0"
|
||||
dependencies:
|
||||
plugin-error: "npm:^1.0.1"
|
||||
through2: "npm:^3.0.1"
|
||||
checksum: 10/0eea1fc60ae7f256184155b61a30a916007d21d37234698d8cdb299f64f71b4d68ca3182528e7da5d71290079c32c0228573578b76f5af7af7230c31537ef9d2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gulp-cli@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "gulp-cli@npm:3.1.0"
|
||||
@@ -9979,7 +9969,6 @@ __metadata:
|
||||
glob: "npm:13.0.6"
|
||||
globals: "npm:17.8.0"
|
||||
gulp: "npm:5.0.1"
|
||||
gulp-brotli: "npm:3.0.0"
|
||||
gulp-json-transform: "npm:0.5.0"
|
||||
gulp-rename: "npm:2.1.0"
|
||||
hls.js: "npm:1.6.16"
|
||||
@@ -12977,17 +12966,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:2 || 3, readable-stream@npm:^3.4.0":
|
||||
version: 3.6.2
|
||||
resolution: "readable-stream@npm:3.6.2"
|
||||
dependencies:
|
||||
inherits: "npm:^2.0.3"
|
||||
string_decoder: "npm:^1.1.1"
|
||||
util-deprecate: "npm:^1.0.1"
|
||||
checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:^2.3.5, readable-stream@npm:~2.3.6":
|
||||
version: 2.3.8
|
||||
resolution: "readable-stream@npm:2.3.8"
|
||||
@@ -13003,6 +12981,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:^3.4.0":
|
||||
version: 3.6.2
|
||||
resolution: "readable-stream@npm:3.6.2"
|
||||
dependencies:
|
||||
inherits: "npm:^2.0.3"
|
||||
string_decoder: "npm:^1.1.1"
|
||||
util-deprecate: "npm:^1.0.1"
|
||||
checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:^4.7.0":
|
||||
version: 4.7.0
|
||||
resolution: "readable-stream@npm:4.7.0"
|
||||
@@ -14561,16 +14550,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"through2@npm:^3.0.1":
|
||||
version: 3.0.2
|
||||
resolution: "through2@npm:3.0.2"
|
||||
dependencies:
|
||||
inherits: "npm:^2.0.4"
|
||||
readable-stream: "npm:2 || 3"
|
||||
checksum: 10/98bdffba8e877fd8beb2154adc4eb0d52fad281130f56f6e5d18f85d1e1aa528a7b27317b302eb5443f6636ab045d3c272e6dffc61d984775db284823b90532d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"time-stamp@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "time-stamp@npm:1.1.0"
|
||||
|
||||
Reference in New Issue
Block a user