mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-26 10:14:54 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a3a43d0d1e |
@@ -240,7 +240,6 @@ gulp.task("rspack-dev-server-e2e-test-app", () =>
|
||||
),
|
||||
contentBase: paths.e2eTestApp_output_root,
|
||||
port: 8095,
|
||||
open: false,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
+5
-6
@@ -28,7 +28,6 @@
|
||||
"test:e2e:show-report": "yarn playwright show-report test/e2e/reports/combined",
|
||||
"test:e2e:demo": "playwright test --config test/e2e/playwright.demo.config.ts",
|
||||
"test:e2e:app": "playwright test --config test/e2e/playwright.app.config.ts",
|
||||
"test:e2e:app:dev": "test/e2e/app/script/develop_app",
|
||||
"test:e2e:gallery": "playwright test --config test/e2e/playwright.gallery.config.ts"
|
||||
},
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
@@ -82,7 +81,6 @@
|
||||
"@tsparticles/engine": "4.2.1",
|
||||
"@tsparticles/preset-links": "4.2.1",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@vvo/tzdb": "6.198.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"barcode-detector": "3.2.0",
|
||||
@@ -99,6 +97,7 @@
|
||||
"echarts": "6.1.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.4.2",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
@@ -173,7 +172,7 @@
|
||||
"eslint": "10.5.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.17.0",
|
||||
"eslint-plugin-import-x": "4.16.2",
|
||||
"eslint-plugin-lit": "2.3.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
@@ -182,7 +181,7 @@
|
||||
"fs-extra": "11.3.5",
|
||||
"generate-license-file": "4.2.1",
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.7.0",
|
||||
"globals": "17.6.0",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
@@ -207,7 +206,7 @@
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.62.0",
|
||||
"typescript-eslint": "8.61.1",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.9",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
@@ -220,7 +219,7 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.21",
|
||||
"globals": "17.7.0",
|
||||
"globals": "17.6.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { timeZonesNames } from "@vvo/tzdb";
|
||||
import timezones from "google-timezones-json";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
|
||||
const RESOLVED_RAW = Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
|
||||
@@ -10,7 +10,7 @@ const RESOLVED_TIME_ZONE =
|
||||
RESOLVED_RAW &&
|
||||
(RESOLVED_RAW === "UTC" ||
|
||||
RESOLVED_RAW === "Etc/UTC" ||
|
||||
timeZonesNames.includes(RESOLVED_RAW))
|
||||
RESOLVED_RAW in timezones)
|
||||
? RESOLVED_RAW
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getTimeZones, timeZonesNames } from "@vvo/tzdb";
|
||||
import timezones from "google-timezones-json";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -13,40 +13,38 @@ const SEARCH_KEYS = [
|
||||
{ name: "secondary", weight: 8 },
|
||||
];
|
||||
|
||||
// @vvo/tzdb is missing the bare "UTC" zone, even though it is a valid IANA
|
||||
// identifier and a common server default. Add UTC back so a
|
||||
// "UTC" configuration can be selected.
|
||||
// google-timezones-json is missing the bare "UTC" and "Etc/UTC" zones, even
|
||||
// though both are valid IANA identifiers and common server defaults. Without
|
||||
// them a "UTC" configuration shows up as an unknown time zone. Add them back.
|
||||
const ADDITIONAL_TIMEZONES: PickerComboBoxItem[] = [
|
||||
{ id: "UTC", primary: "+00:00 UTC", secondary: "UTC" },
|
||||
{ id: "UTC", primary: "(GMT+00:00) UTC", secondary: "UTC" },
|
||||
{ id: "Etc/UTC", primary: "(GMT+00:00) UTC", secondary: "Etc/UTC" },
|
||||
];
|
||||
|
||||
export const getTimezoneOptions = (): PickerComboBoxItem[] => {
|
||||
const options: PickerComboBoxItem[] = Array.from(
|
||||
new Map(
|
||||
getTimeZones({ includeUtc: true })
|
||||
.flatMap((timezone) => {
|
||||
const groupArray = Array.isArray(timezone.group)
|
||||
? timezone.group
|
||||
: [timezone.group];
|
||||
const filteredGroup = groupArray.filter((gName) =>
|
||||
timeZonesNames.includes(gName)
|
||||
);
|
||||
// google-timezones-json also ships an invalid IANA identifier. Correct it so
|
||||
// the zone can be selected (the backend rejects the invalid id).
|
||||
const TIMEZONE_ID_CORRECTIONS: Record<string, string> = {
|
||||
"Asia/Yuzhno-Sakhalinsk": "Asia/Sakhalin",
|
||||
};
|
||||
|
||||
return [timezone.name, ...filteredGroup].map((nameString) => ({
|
||||
id: nameString,
|
||||
primary: timezone.rawFormat,
|
||||
secondary: nameString,
|
||||
}));
|
||||
})
|
||||
.map((item) => [item.id, item])
|
||||
).values()
|
||||
);
|
||||
export const getTimezoneOptions = (): PickerComboBoxItem[] => {
|
||||
const options: PickerComboBoxItem[] = Object.entries(
|
||||
timezones as Record<string, string>
|
||||
).map(([key, value]) => {
|
||||
const id = TIMEZONE_ID_CORRECTIONS[key] ?? key;
|
||||
return {
|
||||
id,
|
||||
primary: value,
|
||||
secondary: id,
|
||||
};
|
||||
});
|
||||
|
||||
for (const timezone of ADDITIONAL_TIMEZONES) {
|
||||
if (!options.some((option) => option.id === timezone.id)) {
|
||||
options.push(timezone);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export interface MediaPlayerItemId {
|
||||
media_content_type?: string | undefined;
|
||||
}
|
||||
|
||||
const MANUAL_ITEM_BASE: Omit<MediaPlayerItem, "title"> = {
|
||||
const MANUAL_ITEM: MediaPlayerItem = {
|
||||
can_expand: true,
|
||||
can_play: false,
|
||||
can_search: false,
|
||||
@@ -83,6 +83,7 @@ const MANUAL_ITEM_BASE: Omit<MediaPlayerItem, "title"> = {
|
||||
media_content_id: MANUAL_MEDIA_SOURCE_PREFIX,
|
||||
media_content_type: "",
|
||||
iconPath: mdiKeyboard,
|
||||
title: "Manual entry",
|
||||
};
|
||||
|
||||
@customElement("ha-media-player-browse")
|
||||
@@ -239,7 +240,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
currentId.media_content_id &&
|
||||
isManualMediaSourceContentId(currentId.media_content_id)
|
||||
) {
|
||||
this._currentItem = this._manualItem();
|
||||
this._currentItem = MANUAL_ITEM;
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: navigateIds,
|
||||
current: this._currentItem,
|
||||
@@ -800,21 +801,12 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
return prom.then((item) => {
|
||||
if (!mediaContentId && this.action === "pick") {
|
||||
item.children = item.children || [];
|
||||
item.children.push(this._manualItem());
|
||||
item.children.push(MANUAL_ITEM);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
private _manualItem(): MediaPlayerItem {
|
||||
return {
|
||||
...MANUAL_ITEM_BASE,
|
||||
title: this.hass.localize(
|
||||
"ui.components.selectors.selector.types.manual"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private _measureCard(): void {
|
||||
this.narrow = (this.dialog ? window.innerWidth : this.offsetWidth) < 450;
|
||||
}
|
||||
|
||||
+16
-6
@@ -27,6 +27,7 @@ export interface LogbookEntry {
|
||||
source?: string; // The trigger source (English phrase, parsed for the cause)
|
||||
domain?: string;
|
||||
state?: string; // The state of the entity
|
||||
attributes?: { event_type?: string }; // Selected attributes the backend surfaces
|
||||
// Context data
|
||||
context_id?: string;
|
||||
context_user_id?: string;
|
||||
@@ -241,13 +242,13 @@ export const parseTriggerSource = (source: string): ParsedTriggerSource => {
|
||||
};
|
||||
|
||||
// Short label shown instead of the bare timestamp for each timestamp-state
|
||||
// domain. Typed to TIMESTAMP_STATE_DOMAINS minus datetime (a real value), so a
|
||||
// new timestamp domain won't compile until it gets a label here.
|
||||
// domain. Typed to TIMESTAMP_STATE_DOMAINS minus datetime (a real value) and
|
||||
// event (handled separately via its event type), so a new timestamp domain
|
||||
// won't compile until it gets a label here.
|
||||
type LogbookActionMessage =
|
||||
| "pressed"
|
||||
| "activated"
|
||||
| "scanned"
|
||||
| "detected_event_no_type"
|
||||
| "updated"
|
||||
| "sent"
|
||||
| "detected"
|
||||
@@ -258,14 +259,13 @@ type LogbookActionMessage =
|
||||
| "command_sent";
|
||||
|
||||
const STATE_ACTION_MESSAGES: Record<
|
||||
Exclude<TimestampStateDomain, "datetime">,
|
||||
Exclude<TimestampStateDomain, "datetime" | "event">,
|
||||
LogbookActionMessage
|
||||
> = {
|
||||
button: "pressed",
|
||||
input_button: "pressed",
|
||||
scene: "activated",
|
||||
tag: "scanned",
|
||||
event: "detected_event_no_type",
|
||||
image: "updated",
|
||||
notify: "sent",
|
||||
wake_word: "detected",
|
||||
@@ -281,8 +281,18 @@ export const localizeStateMessage = (
|
||||
hass: HomeAssistant,
|
||||
state: string,
|
||||
stateObj: HassEntity,
|
||||
domain: string
|
||||
domain: string,
|
||||
attributes?: LogbookEntry["attributes"]
|
||||
): string => {
|
||||
// Events show the triggered event type, falling back to a generic label when
|
||||
// the type is unknown (the timestamp state is meaningless on its own).
|
||||
if (domain === "event") {
|
||||
const eventType = attributes?.event_type;
|
||||
if (eventType != null) {
|
||||
return hass.formatEntityAttributeValue(stateObj, "event_type", eventType);
|
||||
}
|
||||
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event_no_type`);
|
||||
}
|
||||
const actionKey: LogbookActionMessage | undefined =
|
||||
STATE_ACTION_MESSAGES[domain as keyof typeof STATE_ACTION_MESSAGES];
|
||||
if (actionKey) {
|
||||
|
||||
@@ -159,7 +159,7 @@ class DialogEditSidebar extends DirtyStateProviderMixin<SidebarState>()(
|
||||
value: panel.url_path,
|
||||
label:
|
||||
(getPanelTitle(this.hass, panel) || panel.url_path) +
|
||||
`${defaultPanel === panel.url_path ? ` (${this.hass.localize("ui.sidebar.default")})` : ""}`,
|
||||
`${defaultPanel === panel.url_path ? " (default)" : ""}`,
|
||||
icon: getPanelIcon(panel),
|
||||
iconPath: getPanelIconPath(panel),
|
||||
disableHiding: panel.url_path === defaultPanel,
|
||||
|
||||
@@ -41,9 +41,7 @@ export class DialogSupportPackage extends LitElement {
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
width="full"
|
||||
.headerTitle=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.download_support_package"
|
||||
)}
|
||||
header-title="Download support package"
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._supportPackage
|
||||
@@ -54,16 +52,13 @@ export class DialogSupportPackage extends LitElement {
|
||||
: html`
|
||||
<div class="progress-container">
|
||||
<ha-spinner></ha-spinner>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.support_package_generating_preview"
|
||||
)}...
|
||||
Generating preview...
|
||||
</div>
|
||||
`}
|
||||
<div slot="footer" class="footer">
|
||||
<ha-alert>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.support_package_privacy_warning"
|
||||
)}
|
||||
This file may contain personal data about your home. Avoid sharing
|
||||
them with unverified or untrusted parties.
|
||||
</ha-alert>
|
||||
<hr />
|
||||
<ha-dialog-footer>
|
||||
@@ -72,10 +67,10 @@ export class DialogSupportPackage extends LitElement {
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
Close
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._download}>
|
||||
${this.hass.localize("ui.common.download")}
|
||||
Download
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</div>
|
||||
|
||||
@@ -109,10 +109,7 @@ export class SystemLogCard extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<div class="header">
|
||||
<h1 class="card-header">
|
||||
${this.header ||
|
||||
this.hass.localize("ui.panel.config.logs.caption")}
|
||||
</h1>
|
||||
<h1 class="card-header">${this.header || "Logs"}</h1>
|
||||
<div class="header-buttons">
|
||||
<ha-icon-button
|
||||
.path=${mdiDownload}
|
||||
|
||||
@@ -197,9 +197,7 @@ export class HaScriptTrace extends LitElement {
|
||||
</div>
|
||||
|
||||
${this._traces === undefined
|
||||
? html`<div class="container">
|
||||
${this.hass.localize("ui.panel.config.script.trace.loading")}…
|
||||
</div>`
|
||||
? html`<div class="container">Loading…</div>`
|
||||
: this._traces.length === 0
|
||||
? html`<div class="container">
|
||||
${this.hass!.localize(
|
||||
|
||||
@@ -266,7 +266,13 @@ const computeLogbookValue = (
|
||||
if (item.entity_id && item.state) {
|
||||
return {
|
||||
text: stateObj
|
||||
? localizeStateMessage(hass, item.state, stateObj, domain!)
|
||||
? localizeStateMessage(
|
||||
hass,
|
||||
item.state,
|
||||
stateObj,
|
||||
domain!,
|
||||
item.attributes
|
||||
)
|
||||
: item.state,
|
||||
type: "state",
|
||||
};
|
||||
|
||||
@@ -58,9 +58,7 @@ export class HuiErrorBadge extends LitElement implements LovelaceBadge {
|
||||
class="error"
|
||||
@click=${this._viewDetail}
|
||||
type="button"
|
||||
.label=${this.hass?.localize(
|
||||
"ui.panel.lovelace.editor.error_section.title"
|
||||
) ?? ""}
|
||||
label="Error"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiAlertCircle}></ha-svg-icon>
|
||||
<div class="content">${this._config.error}</div>
|
||||
|
||||
@@ -135,11 +135,7 @@ class HuiHistoryChartCardFeature
|
||||
if (this._coordinates && !this._coordinates.length) {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="info">
|
||||
${this.hass!.localize(
|
||||
"ui.components.history_charts.no_history_found"
|
||||
)}
|
||||
</div>
|
||||
<div class="info">No state history found.</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -122,11 +122,7 @@ export class HuiGraphHeaderFooter
|
||||
if (this._coordinates && !this._coordinates.length) {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="info">
|
||||
${this.hass!.localize(
|
||||
"ui.components.history_charts.no_history_found"
|
||||
)}
|
||||
</div>
|
||||
<div class="info">No state history found.</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -47,9 +47,7 @@ export class HuiErrorSection
|
||||
|
||||
// Todo improve
|
||||
return html`
|
||||
<h1>
|
||||
${this.hass!.localize("ui.panel.lovelace.editor.error_section.title")}
|
||||
</h1>
|
||||
<h1>Error</h1>
|
||||
<p>${this._config.error}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -331,28 +331,6 @@ export function computeBarycenter(
|
||||
return totalWeight > 0 ? weightedSum / totalWeight : fallback;
|
||||
}
|
||||
|
||||
// Index of the single highest-weight neighbor present in the reference
|
||||
// section (ties on weight broken by the earliest edge). Used only as a
|
||||
// barycenter tie-break so a node stays beside its dominant neighbor's group
|
||||
// instead of falling back to a stale seed index. Falls back to the node's own
|
||||
// index when it has no resolvable neighbor, matching computeBarycenter.
|
||||
export function dominantNeighborIndex(
|
||||
neighbors: WeightedNeighbor[],
|
||||
referenceIdIndexMap: Map<string, number>,
|
||||
fallback: number
|
||||
): number {
|
||||
let bestIdx = fallback;
|
||||
let bestWeight = -Infinity;
|
||||
neighbors.forEach(({ id, weight }) => {
|
||||
const idx = referenceIdIndexMap.get(id);
|
||||
if (idx !== undefined && weight > bestWeight) {
|
||||
bestWeight = weight;
|
||||
bestIdx = idx;
|
||||
}
|
||||
});
|
||||
return bestIdx;
|
||||
}
|
||||
|
||||
function buildIdIndexMap(section: Node[]): Map<string, number> {
|
||||
const map = new Map<string, number>();
|
||||
section.forEach((node, index) => map.set(node.id, index));
|
||||
@@ -364,20 +342,12 @@ function sortSectionByBarycenter(
|
||||
referenceMap: Map<string, number>,
|
||||
getNeighbors: (node: Node) => WeightedNeighbor[]
|
||||
): { sorted: Node[]; changed: boolean } {
|
||||
const decorated = section.map((node, index) => {
|
||||
const neighbors = getNeighbors(node);
|
||||
return {
|
||||
node,
|
||||
index,
|
||||
barycenter: computeBarycenter(neighbors, referenceMap, index),
|
||||
// Tie-break that keeps a node next to its dominant neighbor's group.
|
||||
anchor: dominantNeighborIndex(neighbors, referenceMap, index),
|
||||
};
|
||||
});
|
||||
decorated.sort(
|
||||
(a, b) =>
|
||||
a.barycenter - b.barycenter || a.anchor - b.anchor || a.index - b.index
|
||||
);
|
||||
const decorated = section.map((node, index) => ({
|
||||
node,
|
||||
index,
|
||||
barycenter: computeBarycenter(getNeighbors(node), referenceMap, index),
|
||||
}));
|
||||
decorated.sort((a, b) => a.barycenter - b.barycenter || a.index - b.index);
|
||||
const sorted = decorated.map((d) => d.node);
|
||||
const changed = sorted.some((n, idx) => n !== section[idx]);
|
||||
return { sorted, changed };
|
||||
@@ -482,55 +452,6 @@ function crossingsAdjacentTo(
|
||||
return total;
|
||||
}
|
||||
|
||||
function countAllCrossings(
|
||||
sections: Node[][],
|
||||
sectionMaps: Map<string, number>[],
|
||||
depths: number[],
|
||||
edges: GraphEdge[]
|
||||
): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < sections.length - 1; i++) {
|
||||
total += countCrossings(
|
||||
getEdgeSegmentsBetween(
|
||||
depths[i],
|
||||
depths[i + 1],
|
||||
depths,
|
||||
edges,
|
||||
sectionMaps[i],
|
||||
sectionMaps[i + 1]
|
||||
)
|
||||
);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// A section is "multi-parent" when at least one of its real nodes draws from
|
||||
// two or more distinct parents in the previous section. In the energy/power/
|
||||
// water cards only the source/home layers do this (grid/solar/battery feed
|
||||
// home + battery_in + grid_return); every later section (floors, areas,
|
||||
// devices) is a pure single-parent tree level. Pass-throughs are always
|
||||
// single-parent (one source/target chain) and never make a section multi-parent.
|
||||
function sectionHasMultipleParents(
|
||||
section: Node[],
|
||||
prevDepth: number,
|
||||
depths: number[]
|
||||
): boolean {
|
||||
return section.some((node) => {
|
||||
if (isPassThroughNode(node)) {
|
||||
return false;
|
||||
}
|
||||
const parentIds = new Set(
|
||||
getNeighborIds(node, "source", prevDepth, depths).map((n) => n.id)
|
||||
);
|
||||
return parentIds.size > 1;
|
||||
});
|
||||
}
|
||||
|
||||
// The head barycenter sweep (STEP 1) only touches the multi-parent head
|
||||
// sections (the single-parent tree below is placed deterministically in
|
||||
// STEP 2), so a single forward+backward pass converges in practice and the loop
|
||||
// early-exits on the first no-change sweep. The cap is just headroom for unusual
|
||||
// topologies with several interacting head sections.
|
||||
const MAX_SORT_ITERATIONS = 4;
|
||||
|
||||
export function sortNodesInSections(
|
||||
@@ -539,30 +460,13 @@ export function sortNodesInSections(
|
||||
edges: GraphEdge[]
|
||||
): Record<number, Node[]> {
|
||||
const sections: Node[][] = depths.map((d) => [...(nodesPerSection[d] || [])]);
|
||||
// Id→index lookup per section, kept in sync with sections.
|
||||
// Id→index lookup per section, kept in sync with sections. Rebuilt only when
|
||||
// a section's order actually changes (inside tryReplace).
|
||||
const sectionMaps: Map<string, number>[] = sections.map(buildIdIndexMap);
|
||||
|
||||
// Classify each section past the root. Multi-parent sections (the
|
||||
// intentionally-ordered source/home layers) are minimized by barycenter in
|
||||
// PASS 2; the rest are single-parent tree levels placed deterministically in
|
||||
// PASS 1. Classification reads only the graph, so it is stable across passes.
|
||||
const multiParent = depths.map(
|
||||
(_d, i) =>
|
||||
i >= 1 && sectionHasMultipleParents(sections[i], depths[i - 1], depths)
|
||||
);
|
||||
|
||||
// Best (fewest-crossing) head ordering seen so far, seeded from the original
|
||||
// input so the head is provably never worse than the seed.
|
||||
const snapshot = (): Node[][] => sections.map((s) => s.slice());
|
||||
let liveCrossings = countAllCrossings(sections, sectionMaps, depths, edges);
|
||||
let bestCrossings = liveCrossings;
|
||||
let bestSections = snapshot();
|
||||
|
||||
// Replace a multi-parent section with a candidate ordering when crossings on
|
||||
// its adjacent boundaries do not increase. Accepting equal-crossing
|
||||
// ("plateau") moves lets the sweep escape local optima; the best snapshot
|
||||
// (captured only on a strict global decrease) is what seeds the head order,
|
||||
// so the result is deterministic and never worse than the seed.
|
||||
// Replace a section with a candidate ordering only when crossings strictly
|
||||
// drop on either side. This keeps user-intended ordering intact when
|
||||
// barycenter would shuffle nodes without improving the layout.
|
||||
const tryReplace = (i: number, candidate: Node[]): boolean => {
|
||||
const before = crossingsAdjacentTo(i, sections, sectionMaps, depths, edges);
|
||||
const sectionSnapshot = sections[i];
|
||||
@@ -570,12 +474,7 @@ export function sortNodesInSections(
|
||||
sections[i] = candidate;
|
||||
sectionMaps[i] = buildIdIndexMap(candidate);
|
||||
const after = crossingsAdjacentTo(i, sections, sectionMaps, depths, edges);
|
||||
if (after <= before) {
|
||||
liveCrossings += after - before;
|
||||
if (liveCrossings < bestCrossings) {
|
||||
bestCrossings = liveCrossings;
|
||||
bestSections = snapshot();
|
||||
}
|
||||
if (after < before) {
|
||||
return true;
|
||||
}
|
||||
sections[i] = sectionSnapshot;
|
||||
@@ -583,97 +482,39 @@ export function sortNodesInSections(
|
||||
return false;
|
||||
};
|
||||
|
||||
// STEP 1 — settle the multi-parent head sections (sources → home/battery_in/
|
||||
// grid_return) by barycenter. These layers have nodes with several parents and
|
||||
// so no single parent position to inherit; we minimize their crossings by the
|
||||
// weighted average of neighbour positions, iterating forward then backward
|
||||
// until the order is stable. The backward sweep is restricted to multi-parent
|
||||
// neighbours, so the head order never depends on its single-parent children —
|
||||
// that is what keeps the whole result idempotent, because STEP 2 re-derives
|
||||
// those children purely from the settled head. The root section (index 0) is
|
||||
// never reordered.
|
||||
if (multiParent.some(Boolean)) {
|
||||
for (let iter = 0; iter < MAX_SORT_ITERATIONS; iter++) {
|
||||
let changed = false;
|
||||
for (let iter = 0; iter < MAX_SORT_ITERATIONS; iter++) {
|
||||
let changed = false;
|
||||
|
||||
for (let i = 1; i < sections.length; i++) {
|
||||
if (!multiParent[i]) {
|
||||
continue;
|
||||
}
|
||||
const prevDepth = depths[i - 1];
|
||||
const result = sortSectionByBarycenter(
|
||||
sections[i],
|
||||
sectionMaps[i - 1],
|
||||
(node) => getNeighborIds(node, "source", prevDepth, depths)
|
||||
);
|
||||
if (result.changed && tryReplace(i, result.sorted)) {
|
||||
changed = true;
|
||||
}
|
||||
for (let i = 1; i < sections.length; i++) {
|
||||
const prevDepth = depths[i - 1];
|
||||
const result = sortSectionByBarycenter(
|
||||
sections[i],
|
||||
sectionMaps[i - 1],
|
||||
(node) => getNeighborIds(node, "source", prevDepth, depths)
|
||||
);
|
||||
if (result.changed && tryReplace(i, result.sorted)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = sections.length - 2; i >= 1; i--) {
|
||||
if (!multiParent[i] || !multiParent[i + 1]) {
|
||||
continue;
|
||||
}
|
||||
const nextDepth = depths[i + 1];
|
||||
const result = sortSectionByBarycenter(
|
||||
sections[i],
|
||||
sectionMaps[i + 1],
|
||||
(node) => getNeighborIds(node, "target", nextDepth, depths)
|
||||
);
|
||||
if (result.changed && tryReplace(i, result.sorted)) {
|
||||
changed = true;
|
||||
}
|
||||
for (let i = sections.length - 2; i >= 0; i--) {
|
||||
const nextDepth = depths[i + 1];
|
||||
const result = sortSectionByBarycenter(
|
||||
sections[i],
|
||||
sectionMaps[i + 1],
|
||||
(node) => getNeighborIds(node, "target", nextDepth, depths)
|
||||
);
|
||||
if (result.changed && tryReplace(i, result.sorted)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed || bestCrossings === 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
// STEP 2 — deterministic hierarchy placement. Starting from the best head
|
||||
// ordering, walk left to right and order every single-parent section by the
|
||||
// position of each node's single parent in the already-final previous section
|
||||
// (sortSectionByBarycenter with one parent reduces to a stable sort by that
|
||||
// parent's index). This is the classic layered-tree drawing: each parent's
|
||||
// children stay contiguous, every single-parent boundary is crossing-free,
|
||||
// pass-throughs travel along their chain, and the user-configured floor/area
|
||||
// order is preserved because same-parent siblings keep their seed index.
|
||||
// It runs unconditionally — grouping a parent's children under it must win
|
||||
// even on a crossing-neutral plateau (the #52852 fix) — and because it is a
|
||||
// pure function of the settled head, re-running yields the identical layout.
|
||||
const finalSections = bestSections.map((s) => s.slice());
|
||||
const finalMaps = finalSections.map(buildIdIndexMap);
|
||||
for (let i = 1; i < finalSections.length; i++) {
|
||||
if (multiParent[i]) {
|
||||
continue;
|
||||
}
|
||||
const prevDepth = depths[i - 1];
|
||||
const { sorted } = sortSectionByBarycenter(
|
||||
finalSections[i],
|
||||
finalMaps[i - 1],
|
||||
(node) => getNeighborIds(node, "source", prevDepth, depths)
|
||||
);
|
||||
finalSections[i] = sorted;
|
||||
finalMaps[i] = buildIdIndexMap(sorted);
|
||||
if (!changed) break;
|
||||
}
|
||||
|
||||
// Hierarchy placement makes every single-parent boundary crossing-free, so on
|
||||
// the energy/water cards (multi-parent only at the head) it can only lower the
|
||||
// total. Guard the general graph: if regrouping somehow raised crossings (only
|
||||
// possible for a multi-parent section sitting *below* single-parent ones,
|
||||
// which the cards never produce), fall back to the gated best head so the
|
||||
// never-worse-than-seed guarantee always holds. Ties keep the grouped layout.
|
||||
const finalCrossings = countAllCrossings(
|
||||
finalSections,
|
||||
finalMaps,
|
||||
depths,
|
||||
edges
|
||||
);
|
||||
const chosen = finalCrossings <= bestCrossings ? finalSections : bestSections;
|
||||
|
||||
const sortedSections: Record<number, Node[]> = {};
|
||||
depths.forEach((depth, i) => {
|
||||
sortedSections[depth] = chosen[i];
|
||||
sortedSections[depth] = sections[i];
|
||||
});
|
||||
return sortedSections;
|
||||
}
|
||||
|
||||
@@ -2503,7 +2503,6 @@
|
||||
"edit_sidebar": "Edit sidebar",
|
||||
"edit_subtitle": "Synced on all devices",
|
||||
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device.",
|
||||
"default": "default",
|
||||
"reset_to_defaults": "Reset to defaults",
|
||||
"reset_confirmation": "Are you sure you want to reset the sidebar to its default configuration? This will restore the original order and visibility of all panels."
|
||||
},
|
||||
@@ -6195,8 +6194,7 @@
|
||||
"paste_invalid_config": "Pasted script is not editable in the visual editor"
|
||||
},
|
||||
"trace": {
|
||||
"edit_script": "Edit script",
|
||||
"loading": "Loading"
|
||||
"edit_script": "Edit script"
|
||||
}
|
||||
},
|
||||
"scene": {
|
||||
@@ -6344,8 +6342,6 @@
|
||||
},
|
||||
"account": {
|
||||
"download_support_package": "Download support package",
|
||||
"support_package_generating_preview": "Generating preview",
|
||||
"support_package_privacy_warning": "This file may contain personal data about your home. Avoid sharing them with unverified or untrusted parties.",
|
||||
"reset_cloud_data": "Reset cloud data",
|
||||
"reset_data_confirm_title": "Reset cloud data?",
|
||||
"reset_data_confirm_text": "This will reset all your cloud settings. This includes your remote connection, Google Assistant, and Amazon Alexa integrations. This action cannot be undone.",
|
||||
@@ -9222,9 +9218,6 @@
|
||||
"title": "Delete section",
|
||||
"text": "This section and all its cards will be deleted."
|
||||
},
|
||||
"error_section": {
|
||||
"title": "Error"
|
||||
},
|
||||
"edit_section": {
|
||||
"header": "Edit section",
|
||||
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_view::tab_visibility%]",
|
||||
|
||||
+23
-102
@@ -5,62 +5,8 @@
|
||||
* yarn test:e2e:app
|
||||
*/
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import type { MoreInfoView } from "../../src/dialogs/more-info/const";
|
||||
import { PANEL_TIMEOUT, QUICK_TIMEOUT, SHELL_TIMEOUT } from "./helpers";
|
||||
|
||||
/**
|
||||
* Each More info view renders one root element inside the dialog, plus one or
|
||||
* more characteristic descendants that prove the view actually populated rather
|
||||
* than rendering an empty shell. `text`, when set, asserts the element's text
|
||||
* instead of just its presence.
|
||||
*/
|
||||
const MORE_INFO_VIEW_ELEMENTS: {
|
||||
view: MoreInfoView;
|
||||
element: string;
|
||||
content: { selector: string; text?: string }[];
|
||||
}[] = [
|
||||
{
|
||||
view: "info",
|
||||
element: "ha-more-info-info",
|
||||
content: [
|
||||
{ selector: "more-info-light" },
|
||||
{ selector: "span.title", text: "Test Light" },
|
||||
],
|
||||
},
|
||||
{
|
||||
view: "history",
|
||||
element: "ha-more-info-history-and-logbook",
|
||||
// The demo loads the history component but not logbook.
|
||||
content: [{ selector: "ha-more-info-history" }],
|
||||
},
|
||||
{
|
||||
view: "settings",
|
||||
element: "ha-more-info-settings",
|
||||
// The scenario mocks config/entity_registry/get, so the real registry
|
||||
// panel renders instead of the "no unique ID" warning.
|
||||
content: [{ selector: "entity-registry-settings" }],
|
||||
},
|
||||
{
|
||||
view: "related",
|
||||
element: "ha-related-items",
|
||||
// search/related is mocked to return no relations, so the empty list
|
||||
// renders.
|
||||
content: [{ selector: "ha-related-items >> ha-list" }],
|
||||
},
|
||||
{
|
||||
view: "add_to",
|
||||
element: "ha-more-info-add-to",
|
||||
// Admin users get the default add-to action list.
|
||||
content: [{ selector: "ha-add-to-action-list" }],
|
||||
},
|
||||
{
|
||||
view: "details",
|
||||
element: "ha-more-info-details",
|
||||
// The details view renders the state and attributes cards.
|
||||
content: [{ selector: "ha-card" }],
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -210,56 +156,31 @@ test.describe("Lovelace dashboard", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe("Light more-info dialog", () => {
|
||||
for (const { view, element, content } of MORE_INFO_VIEW_ELEMENTS) {
|
||||
test(`opens more-info ${view} view for a light entity`, async ({
|
||||
page,
|
||||
}) => {
|
||||
// The light-more-info scenario seeds light.test_light synchronously.
|
||||
await goToPanel(page, "/?scenario=light-more-info#/lovelace");
|
||||
test("opens more-info dialog for a light entity", async ({ page }) => {
|
||||
// The light-more-info scenario seeds light.test_light synchronously.
|
||||
await goToPanel(page, "/?scenario=light-more-info#/lovelace");
|
||||
|
||||
const dialog = page.locator("ha-more-info-dialog");
|
||||
|
||||
// Fire the standard hass-more-info event from the app root with an
|
||||
// explicit view. The HA shell opens ha-more-info-dialog on the requested
|
||||
// view directly, so the test does not depend on the admin/demo-hidden
|
||||
// header controls.
|
||||
//
|
||||
// The event is one-shot: if it lands before the shell's hass-more-info
|
||||
// listener is attached it is silently dropped. Re-dispatching is
|
||||
// idempotent (showDialog just resets the dialog to the requested view),
|
||||
// so poll the dispatch until the requested view actually renders.
|
||||
await expect(async () => {
|
||||
await page.evaluate((v) => {
|
||||
const el = document.querySelector("ha-test");
|
||||
el?.dispatchEvent(
|
||||
new CustomEvent("hass-more-info", {
|
||||
detail: { entityId: "light.test_light", view: v },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}, view);
|
||||
|
||||
await expect(dialog).toBeAttached({ timeout: QUICK_TIMEOUT });
|
||||
await expect(dialog.locator(element)).toBeAttached({
|
||||
timeout: QUICK_TIMEOUT,
|
||||
});
|
||||
}).toPass({ timeout: SHELL_TIMEOUT });
|
||||
|
||||
// Each view should render its own characteristic content, not just an
|
||||
// empty shell.
|
||||
for (const { selector, text } of content) {
|
||||
const locator = dialog.locator(selector).first();
|
||||
if (text) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await expect(locator).toContainText(text, { timeout: QUICK_TIMEOUT });
|
||||
} else {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await expect(locator).toBeAttached({ timeout: QUICK_TIMEOUT });
|
||||
}
|
||||
}
|
||||
// Fire the standard hass-more-info event from the app root. The HA shell
|
||||
// listens for this and opens ha-more-info-dialog via its dialog manager.
|
||||
await page.evaluate(() => {
|
||||
const el = document.querySelector("ha-test");
|
||||
el?.dispatchEvent(
|
||||
new CustomEvent("hass-more-info", {
|
||||
detail: { entityId: "light.test_light" },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const dialog = page.locator("ha-more-info-dialog");
|
||||
await expect(dialog).toBeAttached({ timeout: SHELL_TIMEOUT });
|
||||
|
||||
// Confirm it actually rendered our entity, not a generic empty dialog.
|
||||
await expect(dialog.locator("span.title")).toContainText("Test Light", {
|
||||
timeout: QUICK_TIMEOUT,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Develop the e2e test app
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../../../.."
|
||||
|
||||
./node_modules/.bin/gulp develop-e2e-test-app
|
||||
@@ -25,7 +25,6 @@ import { mockLovelace } from "../../../../demo/src/stubs/lovelace";
|
||||
import { mockMediaPlayer } from "../../../../demo/src/stubs/media_player";
|
||||
import { mockPersistentNotification } from "../../../../demo/src/stubs/persistent_notification";
|
||||
import { mockRecorder } from "../../../../demo/src/stubs/recorder";
|
||||
import { mockSearch } from "../../../../demo/src/stubs/search";
|
||||
import { mockSensor } from "../../../../demo/src/stubs/sensor";
|
||||
import { mockSystemLog } from "../../../../demo/src/stubs/system_log";
|
||||
import { mockTemplate } from "../../../../demo/src/stubs/template";
|
||||
@@ -101,7 +100,6 @@ export class HaTest extends HomeAssistantAppEl {
|
||||
mockConfigEntries(hass);
|
||||
mockIcons(hass);
|
||||
mockPersistentNotification(hass);
|
||||
mockSearch(hass);
|
||||
|
||||
// Load default entities from the sections config
|
||||
hass.addEntities(energyEntities());
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ExtEntityRegistryEntry } from "../../../../../src/data/entity/entity_registry";
|
||||
import type { MockHomeAssistant } from "../../../../../src/fake_data/provide_hass";
|
||||
|
||||
export type Scenario = (hass: MockHomeAssistant) => Promise<void> | void;
|
||||
@@ -57,36 +56,6 @@ const lightMoreInfoScenario: Scenario = async (hass) => {
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// The base entity registry stub only mocks the list/get_entries commands, so
|
||||
// the more-info settings view falls back to its "no unique ID" warning. Mock
|
||||
// the single-entry lookup (config/entity_registry/get) so the settings view
|
||||
// renders the real entity-registry-settings panel.
|
||||
const registryEntry: ExtEntityRegistryEntry = {
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
id: "test_light",
|
||||
entity_id: "light.test_light",
|
||||
unique_id: "test_light_unique_id",
|
||||
name: null,
|
||||
icon: null,
|
||||
platform: "demo",
|
||||
config_entry_id: null,
|
||||
config_subentry_id: null,
|
||||
device_id: null,
|
||||
area_id: null,
|
||||
labels: [],
|
||||
disabled_by: null,
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
original_name: "Test Light",
|
||||
options: null,
|
||||
categories: {},
|
||||
capabilities: {},
|
||||
aliases: [],
|
||||
};
|
||||
hass.mockWS("config/entity_registry/get", () => registryEntry);
|
||||
};
|
||||
|
||||
// ── Registry ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
getPassThroughSections,
|
||||
createPassThroughNode,
|
||||
computeBarycenter,
|
||||
dominantNeighborIndex,
|
||||
sortNodesInSections,
|
||||
} from "../../../../../src/resources/echarts/components/sankey/sankey-layout";
|
||||
|
||||
@@ -757,470 +756,5 @@ describe("Sankey Layout Functions", () => {
|
||||
});
|
||||
expectIdentityPreserved(result, input);
|
||||
});
|
||||
|
||||
it("untangles a plateau-trapped subtree to remove an avoidable crossing (#52852)", () => {
|
||||
// Realistic consumption tree: home → floors → areas → devices, plus two
|
||||
// devices that attach higher up — one on a floor with no area
|
||||
// (dev_floor_outside) and one straight on home (dev_home) — which the
|
||||
// engine threads through with pass-throughs. The seed splits
|
||||
// floor_outside's subtree: its pass-through child sits *after*
|
||||
// floor_foundation's area. Pulling it back trades a crossing from the
|
||||
// (1,2) boundary to the (2,3) boundary — a net-zero "plateau" the old
|
||||
// strict gate refused, leaving the crossing. The plateau-escape must now
|
||||
// take that step and let the device section follow, reaching 0 crossings.
|
||||
const e = {
|
||||
homeFo: { source: "home", target: "floor_outside", value: 1 },
|
||||
homeFf: { source: "home", target: "floor_foundation", value: 1 },
|
||||
foHvac: { source: "floor_outside", target: "area_hvac", value: 1 },
|
||||
ffParking: {
|
||||
source: "floor_foundation",
|
||||
target: "area_parking",
|
||||
value: 1,
|
||||
},
|
||||
hvacDev: { source: "area_hvac", target: "dev_hvac", value: 1 },
|
||||
parkingDev: { source: "area_parking", target: "dev_parking", value: 1 },
|
||||
foDev: {
|
||||
source: "floor_outside",
|
||||
target: "dev_floor_outside",
|
||||
value: 1,
|
||||
},
|
||||
homeDev: { source: "home", target: "dev_home", value: 1 },
|
||||
};
|
||||
const testNodes: Record<string, TestNode> = {
|
||||
home: {
|
||||
id: "home",
|
||||
depth: 0,
|
||||
value: 4,
|
||||
inEdges: [],
|
||||
outEdges: [e.homeFo, e.homeFf, e.homeDev],
|
||||
},
|
||||
floor_outside: {
|
||||
id: "floor_outside",
|
||||
depth: 1,
|
||||
value: 2,
|
||||
inEdges: [e.homeFo],
|
||||
outEdges: [e.foHvac, e.foDev],
|
||||
},
|
||||
floor_foundation: {
|
||||
id: "floor_foundation",
|
||||
depth: 1,
|
||||
value: 1,
|
||||
inEdges: [e.homeFf],
|
||||
outEdges: [e.ffParking],
|
||||
},
|
||||
area_hvac: {
|
||||
id: "area_hvac",
|
||||
depth: 2,
|
||||
value: 1,
|
||||
inEdges: [e.foHvac],
|
||||
outEdges: [e.hvacDev],
|
||||
},
|
||||
area_parking: {
|
||||
id: "area_parking",
|
||||
depth: 2,
|
||||
value: 1,
|
||||
inEdges: [e.ffParking],
|
||||
outEdges: [e.parkingDev],
|
||||
},
|
||||
dev_hvac: {
|
||||
id: "dev_hvac",
|
||||
depth: 3,
|
||||
value: 1,
|
||||
inEdges: [e.hvacDev],
|
||||
outEdges: [],
|
||||
},
|
||||
dev_parking: {
|
||||
id: "dev_parking",
|
||||
depth: 3,
|
||||
value: 1,
|
||||
inEdges: [e.parkingDev],
|
||||
outEdges: [],
|
||||
},
|
||||
dev_floor_outside: {
|
||||
id: "dev_floor_outside",
|
||||
depth: 3,
|
||||
value: 1,
|
||||
inEdges: [e.foDev],
|
||||
outEdges: [],
|
||||
},
|
||||
dev_home: {
|
||||
id: "dev_home",
|
||||
depth: 3,
|
||||
value: 1,
|
||||
inEdges: [e.homeDev],
|
||||
outEdges: [],
|
||||
},
|
||||
};
|
||||
const { nodes: graph, edges } = buildGraph(testNodes);
|
||||
const ptHome1 = createPassThroughNode("home", "dev_home", 1, 1);
|
||||
const ptFo2 = createPassThroughNode(
|
||||
"floor_outside",
|
||||
"dev_floor_outside",
|
||||
2,
|
||||
1
|
||||
);
|
||||
const ptHome2 = createPassThroughNode("home", "dev_home", 2, 1);
|
||||
|
||||
// Seed order from ha-sankey-chart: pass-throughs appended after the real
|
||||
// children, so floor_outside's subtree is broken across the section.
|
||||
const input = {
|
||||
0: [graph.home],
|
||||
1: [graph.floor_outside, graph.floor_foundation, ptHome1],
|
||||
2: [graph.area_hvac, graph.area_parking, ptFo2, ptHome2],
|
||||
3: [
|
||||
graph.dev_hvac,
|
||||
graph.dev_parking,
|
||||
graph.dev_floor_outside,
|
||||
graph.dev_home,
|
||||
],
|
||||
};
|
||||
const result = sortNodesInSections(input, [0, 1, 2, 3], edges);
|
||||
|
||||
// floor_outside's children (area_hvac and its pass-through) are now
|
||||
// contiguous, ahead of floor_foundation's; the layout is crossing-free.
|
||||
expect(sectionIds(result)).toEqual({
|
||||
0: ["home"],
|
||||
1: ["floor_outside", "floor_foundation", "home-dev_home-1"],
|
||||
2: [
|
||||
"area_hvac",
|
||||
"floor_outside-dev_floor_outside-2",
|
||||
"area_parking",
|
||||
"home-dev_home-2",
|
||||
],
|
||||
3: ["dev_hvac", "dev_floor_outside", "dev_parking", "dev_home"],
|
||||
});
|
||||
expectIdentityPreserved(result, input);
|
||||
|
||||
// Re-running on the result must not drift: plateau churn is discarded and
|
||||
// the best snapshot is returned, so the order is stable (idempotent).
|
||||
const again = sortNodesInSections(result, [0, 1, 2, 3], edges);
|
||||
expect(sectionIds(again)).toEqual(sectionIds(result));
|
||||
});
|
||||
|
||||
it("groups single-parent siblings under their parent and keeps configured sibling order", () => {
|
||||
// Two floors, two areas each, fed to the engine interleaved (not grouped
|
||||
// by floor). The deterministic hierarchy pass must regroup areas under
|
||||
// their floor, and within a floor preserve the configured (seed) order:
|
||||
// a1 before a2, b1 before b2.
|
||||
const e = {
|
||||
hFa: { source: "home", target: "floor_a", value: 2 },
|
||||
hFb: { source: "home", target: "floor_b", value: 2 },
|
||||
faA1: { source: "floor_a", target: "a1", value: 1 },
|
||||
faA2: { source: "floor_a", target: "a2", value: 1 },
|
||||
fbB1: { source: "floor_b", target: "b1", value: 1 },
|
||||
fbB2: { source: "floor_b", target: "b2", value: 1 },
|
||||
};
|
||||
const testNodes: Record<string, TestNode> = {
|
||||
home: {
|
||||
id: "home",
|
||||
depth: 0,
|
||||
value: 4,
|
||||
inEdges: [],
|
||||
outEdges: [e.hFa, e.hFb],
|
||||
},
|
||||
floor_a: {
|
||||
id: "floor_a",
|
||||
depth: 1,
|
||||
value: 2,
|
||||
inEdges: [e.hFa],
|
||||
outEdges: [e.faA1, e.faA2],
|
||||
},
|
||||
floor_b: {
|
||||
id: "floor_b",
|
||||
depth: 1,
|
||||
value: 2,
|
||||
inEdges: [e.hFb],
|
||||
outEdges: [e.fbB1, e.fbB2],
|
||||
},
|
||||
a1: { id: "a1", depth: 2, value: 1, inEdges: [e.faA1], outEdges: [] },
|
||||
a2: { id: "a2", depth: 2, value: 1, inEdges: [e.faA2], outEdges: [] },
|
||||
b1: { id: "b1", depth: 2, value: 1, inEdges: [e.fbB1], outEdges: [] },
|
||||
b2: { id: "b2", depth: 2, value: 1, inEdges: [e.fbB2], outEdges: [] },
|
||||
};
|
||||
const { nodes: graph, edges } = buildGraph(testNodes);
|
||||
const input = {
|
||||
0: [graph.home],
|
||||
1: [graph.floor_a, graph.floor_b],
|
||||
2: [graph.a1, graph.b1, graph.a2, graph.b2], // interleaved
|
||||
};
|
||||
const result = sortNodesInSections(input, [0, 1, 2], edges);
|
||||
|
||||
expect(sectionIds(result)).toEqual({
|
||||
0: ["home"],
|
||||
1: ["floor_a", "floor_b"],
|
||||
2: ["a1", "a2", "b1", "b2"],
|
||||
});
|
||||
expectIdentityPreserved(result, input);
|
||||
});
|
||||
|
||||
it("orders a single-parent section by parent position, ignoring flow magnitude", () => {
|
||||
// childB carries a far larger flow than childA, but a single-parent
|
||||
// section is ordered by parent position (hierarchy), never by value.
|
||||
const edgeACa = { source: "A", target: "childA", value: 1 };
|
||||
const edgeBCb = { source: "B", target: "childB", value: 100 };
|
||||
const testNodes: Record<string, TestNode> = {
|
||||
A: { id: "A", depth: 0, value: 1, inEdges: [], outEdges: [edgeACa] },
|
||||
B: { id: "B", depth: 0, value: 100, inEdges: [], outEdges: [edgeBCb] },
|
||||
childA: {
|
||||
id: "childA",
|
||||
depth: 1,
|
||||
value: 1,
|
||||
inEdges: [edgeACa],
|
||||
outEdges: [],
|
||||
},
|
||||
childB: {
|
||||
id: "childB",
|
||||
depth: 1,
|
||||
value: 100,
|
||||
inEdges: [edgeBCb],
|
||||
outEdges: [],
|
||||
},
|
||||
};
|
||||
const { nodes: graph, edges } = buildGraph(testNodes);
|
||||
const input = { 0: [graph.A, graph.B], 1: [graph.childB, graph.childA] };
|
||||
const result = sortNodesInSections(input, [0, 1], edges);
|
||||
|
||||
expect(sectionIds(result)).toEqual({
|
||||
0: ["A", "B"],
|
||||
1: ["childA", "childB"],
|
||||
});
|
||||
expectIdentityPreserved(result, input);
|
||||
});
|
||||
|
||||
it("keeps the single-parent tree hierarchical below a multi-parent source layer", () => {
|
||||
// grid + solar feed home (a genuine multi-parent section that stays under
|
||||
// the barycenter sweep); the floor/area tree below is single-parent and
|
||||
// must regroup by parent regardless of the multi-parent head.
|
||||
const e = {
|
||||
gH: { source: "grid", target: "home", value: 2 },
|
||||
sH: { source: "solar", target: "home", value: 2 },
|
||||
hFa: { source: "home", target: "floor_a", value: 2 },
|
||||
hFb: { source: "home", target: "floor_b", value: 2 },
|
||||
faA: { source: "floor_a", target: "area_a", value: 2 },
|
||||
fbB: { source: "floor_b", target: "area_b", value: 2 },
|
||||
};
|
||||
const testNodes: Record<string, TestNode> = {
|
||||
grid: { id: "grid", depth: 0, value: 2, inEdges: [], outEdges: [e.gH] },
|
||||
solar: {
|
||||
id: "solar",
|
||||
depth: 0,
|
||||
value: 2,
|
||||
inEdges: [],
|
||||
outEdges: [e.sH],
|
||||
},
|
||||
home: {
|
||||
id: "home",
|
||||
depth: 1,
|
||||
value: 4,
|
||||
inEdges: [e.gH, e.sH],
|
||||
outEdges: [e.hFa, e.hFb],
|
||||
},
|
||||
floor_a: {
|
||||
id: "floor_a",
|
||||
depth: 2,
|
||||
value: 2,
|
||||
inEdges: [e.hFa],
|
||||
outEdges: [e.faA],
|
||||
},
|
||||
floor_b: {
|
||||
id: "floor_b",
|
||||
depth: 2,
|
||||
value: 2,
|
||||
inEdges: [e.hFb],
|
||||
outEdges: [e.fbB],
|
||||
},
|
||||
area_a: {
|
||||
id: "area_a",
|
||||
depth: 3,
|
||||
value: 2,
|
||||
inEdges: [e.faA],
|
||||
outEdges: [],
|
||||
},
|
||||
area_b: {
|
||||
id: "area_b",
|
||||
depth: 3,
|
||||
value: 2,
|
||||
inEdges: [e.fbB],
|
||||
outEdges: [],
|
||||
},
|
||||
};
|
||||
const { nodes: graph, edges } = buildGraph(testNodes);
|
||||
const input = {
|
||||
0: [graph.grid, graph.solar],
|
||||
1: [graph.home],
|
||||
2: [graph.floor_a, graph.floor_b],
|
||||
3: [graph.area_b, graph.area_a], // reversed; must regroup under floors
|
||||
};
|
||||
const result = sortNodesInSections(input, [0, 1, 2, 3], edges);
|
||||
|
||||
expect(sectionIds(result)).toEqual({
|
||||
0: ["grid", "solar"],
|
||||
1: ["home"],
|
||||
2: ["floor_a", "floor_b"],
|
||||
3: ["area_a", "area_b"],
|
||||
});
|
||||
expectIdentityPreserved(result, input);
|
||||
});
|
||||
|
||||
it("regroups single-parent children after a multi-parent head is reordered (idempotent)", () => {
|
||||
// A multi-parent head section [H0,H1,H2] (only H1 draws from two sources)
|
||||
// gets reordered by the barycenter sweep. Its single-parent children must
|
||||
// then be regrouped under the *settled* head — H1's children before H2's
|
||||
// child — and the result must be idempotent. Before the head/tree passes
|
||||
// were ordered correctly the children stayed grouped against the head's
|
||||
// SEED order, which both mis-grouped them and broke f(f(x)) === f(x).
|
||||
const e = {
|
||||
s0h0: { source: "S0", target: "H0", value: 6 },
|
||||
s0h1: { source: "S0", target: "H1", value: 7 },
|
||||
s1h1: { source: "S1", target: "H1", value: 5 },
|
||||
s2h2: { source: "S2", target: "H2", value: 6 },
|
||||
h1d0: { source: "H1", target: "D0", value: 7 },
|
||||
h1d2: { source: "H1", target: "D2", value: 9 },
|
||||
h2d1: { source: "H2", target: "D1", value: 7 },
|
||||
};
|
||||
const testNodes: Record<string, TestNode> = {
|
||||
S0: {
|
||||
id: "S0",
|
||||
depth: 0,
|
||||
value: 13,
|
||||
inEdges: [],
|
||||
outEdges: [e.s0h0, e.s0h1],
|
||||
},
|
||||
S1: { id: "S1", depth: 0, value: 5, inEdges: [], outEdges: [e.s1h1] },
|
||||
S2: { id: "S2", depth: 0, value: 6, inEdges: [], outEdges: [e.s2h2] },
|
||||
H0: { id: "H0", depth: 1, value: 6, inEdges: [e.s0h0], outEdges: [] },
|
||||
H1: {
|
||||
id: "H1",
|
||||
depth: 1,
|
||||
value: 12,
|
||||
inEdges: [e.s1h1, e.s0h1],
|
||||
outEdges: [e.h1d0, e.h1d2],
|
||||
},
|
||||
H2: {
|
||||
id: "H2",
|
||||
depth: 1,
|
||||
value: 6,
|
||||
inEdges: [e.s2h2],
|
||||
outEdges: [e.h2d1],
|
||||
},
|
||||
D0: { id: "D0", depth: 2, value: 7, inEdges: [e.h1d0], outEdges: [] },
|
||||
D1: { id: "D1", depth: 2, value: 7, inEdges: [e.h2d1], outEdges: [] },
|
||||
D2: { id: "D2", depth: 2, value: 9, inEdges: [e.h1d2], outEdges: [] },
|
||||
};
|
||||
const { nodes: graph, edges } = buildGraph(testNodes);
|
||||
const input = {
|
||||
0: [graph.S0, graph.S1, graph.S2],
|
||||
1: [graph.H2, graph.H1, graph.H0],
|
||||
2: [graph.D1, graph.D2, graph.D0],
|
||||
};
|
||||
const result = sortNodesInSections(input, [0, 1, 2], edges);
|
||||
|
||||
// Head settles to barycenter order [H0,H1,H2]; children regroup under it:
|
||||
// H1's children (D2,D0) precede H2's child (D1).
|
||||
expect(sectionIds(result)).toEqual({
|
||||
0: ["S0", "S1", "S2"],
|
||||
1: ["H0", "H1", "H2"],
|
||||
2: ["D2", "D0", "D1"],
|
||||
});
|
||||
expectIdentityPreserved(result, input);
|
||||
|
||||
// Idempotent: re-feeding the output yields the identical order.
|
||||
const again = sortNodesInSections(result, [0, 1, 2], edges);
|
||||
expect(sectionIds(again)).toEqual(sectionIds(result));
|
||||
});
|
||||
|
||||
it("lets single-parent children follow their reordered multi-parent parent to reach 0 crossings", () => {
|
||||
// root [a,b,c,d]; section 1 [m1,m2] is multi-parent with a clean split
|
||||
// (c,d -> m1 ; a,b -> m2); section 2 [x<-m1, y<-m2] single-parent. The
|
||||
// optimum needs BOTH the parent order swapped (m2,m1) AND the children to
|
||||
// follow (y,x) — placing the tree after the head settles reaches it.
|
||||
const e = {
|
||||
am2: { source: "a", target: "m2", value: 1 },
|
||||
bm2: { source: "b", target: "m2", value: 1 },
|
||||
cm1: { source: "c", target: "m1", value: 1 },
|
||||
dm1: { source: "d", target: "m1", value: 1 },
|
||||
m1x: { source: "m1", target: "x", value: 2 },
|
||||
m2y: { source: "m2", target: "y", value: 2 },
|
||||
};
|
||||
const testNodes: Record<string, TestNode> = {
|
||||
a: { id: "a", depth: 0, value: 1, inEdges: [], outEdges: [e.am2] },
|
||||
b: { id: "b", depth: 0, value: 1, inEdges: [], outEdges: [e.bm2] },
|
||||
c: { id: "c", depth: 0, value: 1, inEdges: [], outEdges: [e.cm1] },
|
||||
d: { id: "d", depth: 0, value: 1, inEdges: [], outEdges: [e.dm1] },
|
||||
m1: {
|
||||
id: "m1",
|
||||
depth: 1,
|
||||
value: 2,
|
||||
inEdges: [e.cm1, e.dm1],
|
||||
outEdges: [e.m1x],
|
||||
},
|
||||
m2: {
|
||||
id: "m2",
|
||||
depth: 1,
|
||||
value: 2,
|
||||
inEdges: [e.am2, e.bm2],
|
||||
outEdges: [e.m2y],
|
||||
},
|
||||
x: { id: "x", depth: 2, value: 2, inEdges: [e.m1x], outEdges: [] },
|
||||
y: { id: "y", depth: 2, value: 2, inEdges: [e.m2y], outEdges: [] },
|
||||
};
|
||||
const { nodes: graph, edges } = buildGraph(testNodes);
|
||||
const input = {
|
||||
0: [graph.a, graph.b, graph.c, graph.d],
|
||||
1: [graph.m1, graph.m2],
|
||||
2: [graph.x, graph.y],
|
||||
};
|
||||
const result = sortNodesInSections(input, [0, 1, 2], edges);
|
||||
|
||||
expect(sectionIds(result)).toEqual({
|
||||
0: ["a", "b", "c", "d"],
|
||||
1: ["m2", "m1"],
|
||||
2: ["y", "x"],
|
||||
});
|
||||
expectIdentityPreserved(result, input);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dominantNeighborIndex", () => {
|
||||
it("returns the index of the single heaviest neighbor", () => {
|
||||
const map = new Map([
|
||||
["light", 0],
|
||||
["heavy", 3],
|
||||
]);
|
||||
const result = dominantNeighborIndex(
|
||||
[
|
||||
{ id: "light", weight: 1 },
|
||||
{ id: "heavy", weight: 5 },
|
||||
],
|
||||
map,
|
||||
9
|
||||
);
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
|
||||
it("breaks weight ties by the earliest edge", () => {
|
||||
const map = new Map([
|
||||
["a", 2],
|
||||
["b", 4],
|
||||
]);
|
||||
const result = dominantNeighborIndex(
|
||||
[
|
||||
{ id: "a", weight: 1 },
|
||||
{ id: "b", weight: 1 },
|
||||
],
|
||||
map,
|
||||
9
|
||||
);
|
||||
expect(result).toBe(2);
|
||||
});
|
||||
|
||||
it("falls back when no neighbor is in the reference section", () => {
|
||||
const result = dominantNeighborIndex(
|
||||
[{ id: "missing", weight: 1 }],
|
||||
new Map(),
|
||||
7
|
||||
);
|
||||
expect(result).toBe(7);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4187,6 +4187,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@package-json/types@npm:^0.0.12":
|
||||
version: 0.0.12
|
||||
resolution: "@package-json/types@npm:0.0.12"
|
||||
checksum: 10/435d921b3ccc817ebe282c6eaac50282691d3be4dafb461d01c03b89fb89cda14ba7b5e20d01503bc1996ab93f98ae8e71601c3be7119b4ea76193b550523fcd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@playwright/test@npm:1.61.0":
|
||||
version: 1.61.0
|
||||
resolution: "@playwright/test@npm:1.61.0"
|
||||
@@ -5712,105 +5719,105 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:8.62.0":
|
||||
version: 8.62.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.62.0"
|
||||
"@typescript-eslint/eslint-plugin@npm:8.61.1":
|
||||
version: 8.61.1
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.61.1"
|
||||
dependencies:
|
||||
"@eslint-community/regexpp": "npm:^4.12.2"
|
||||
"@typescript-eslint/scope-manager": "npm:8.62.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.62.0"
|
||||
"@typescript-eslint/utils": "npm:8.62.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.62.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.61.1"
|
||||
"@typescript-eslint/type-utils": "npm:8.61.1"
|
||||
"@typescript-eslint/utils": "npm:8.61.1"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.61.1"
|
||||
ignore: "npm:^7.0.5"
|
||||
natural-compare: "npm:^1.4.0"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
"@typescript-eslint/parser": ^8.62.0
|
||||
"@typescript-eslint/parser": ^8.61.1
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/a093962c84e49d7524078a97c3ecfdedfaa217a6f68047d3eedb29677425210acfacaa2fe88f447e9662063979f31c8268e4568caca038df09deee9f06124d7f
|
||||
checksum: 10/5434b78781f750eb1e2918f960ff3a6a3fae36951591456b1a309695a5c6c027d914038d7c2c71e614611b5c46a3be85b4b004581be0bcfb1be84e741b0e98a8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:8.62.0":
|
||||
version: 8.62.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.62.0"
|
||||
"@typescript-eslint/parser@npm:8.61.1":
|
||||
version: 8.61.1
|
||||
resolution: "@typescript-eslint/parser@npm:8.61.1"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": "npm:8.62.0"
|
||||
"@typescript-eslint/types": "npm:8.62.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.62.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.62.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.61.1"
|
||||
"@typescript-eslint/types": "npm:8.61.1"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.61.1"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.61.1"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/d0abbf12080532f6460af186098fab15f1f3695ee4817f96209ecfb00ef7ec89ec476051bde35a396217c8e37d5e441f3814807eb082e11904a0a1dc4b6d3b14
|
||||
checksum: 10/cdaca9bb78bd6cc7210e88b28c42af11b23a47393a97ed37350e64a3846d036ebd178583fd2a54216974a740b3f6932274bdaf72046e6307ef26aee3ebe35cec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/project-service@npm:8.62.0":
|
||||
version: 8.62.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.62.0"
|
||||
"@typescript-eslint/project-service@npm:8.61.1":
|
||||
version: 8.61.1
|
||||
resolution: "@typescript-eslint/project-service@npm:8.61.1"
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.62.0"
|
||||
"@typescript-eslint/types": "npm:^8.62.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.61.1"
|
||||
"@typescript-eslint/types": "npm:^8.61.1"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/e296f3aaaf7b4fc56e6410420a98a995f59bf45187445c9ad94d76de557a47071558869414c8ec179dfefce4f65ef8c15fcda7db653ed8fb95ff25b8119f9bb1
|
||||
checksum: 10/51eb5cbd74748d08512db976bbeabdb9352f44e220621dce3bc96837bc309c7d266df49007be196e57950cf9e12e2c574649cbf14aa2e518734ee55ff7d86f2c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.62.0":
|
||||
version: 8.62.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.62.0"
|
||||
"@typescript-eslint/scope-manager@npm:8.61.1":
|
||||
version: 8.61.1
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.61.1"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.62.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.62.0"
|
||||
checksum: 10/6477062eb056986c9f94b35761c0b67bb9995798ba94c5d2bcb01932e525604715ce62e816468b2c80a8f05daa33b3339ea40646a31f733ea9840cee1dd3e82d
|
||||
"@typescript-eslint/types": "npm:8.61.1"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.61.1"
|
||||
checksum: 10/69c1d5b403b2e6adbae7e24856628941e912304ac728b6beae959df98092707adf3f60e1d0c9b90badd758d76174e9d44a7eba55608693c976e5cba5cc47593c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.62.0, @typescript-eslint/tsconfig-utils@npm:^8.62.0":
|
||||
version: 8.62.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.62.0"
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.61.1, @typescript-eslint/tsconfig-utils@npm:^8.61.1":
|
||||
version: 8.61.1
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.61.1"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/578f486df8eb2d2ec3939afc37102b89521d531d409d76e30a8ac3e9f48a3ae410e19e40c2aba3810f28391925a35ed391204ff786cc230542de82817efafda0
|
||||
checksum: 10/ee81d01809178d6fd88a478bdf8fb546063c55f01385c6443fe7b93ebe9ba26ecda4f0eb804b2fc0a189dc34a51e89690477fb68cd099cd3952902f376d641c6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:8.62.0":
|
||||
version: 8.62.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.62.0"
|
||||
"@typescript-eslint/type-utils@npm:8.61.1":
|
||||
version: 8.61.1
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.61.1"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.62.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.62.0"
|
||||
"@typescript-eslint/utils": "npm:8.62.0"
|
||||
"@typescript-eslint/types": "npm:8.61.1"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.61.1"
|
||||
"@typescript-eslint/utils": "npm:8.61.1"
|
||||
debug: "npm:^4.4.3"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/e1627d2bd792cf856c36db4f4e9c89e3111ad9bf81fc7489e957d26d36f89ab00226eee1838ee499947a898e37c1f30338bca4fa05ca437154c1813d54831b7e
|
||||
checksum: 10/01c62227479d94ed3745e3bef5a0c870586d75b6ed550e4beb84b25df23de29558e0bdb6ff291e457977254764766c5d252b75a03d4ad592998269dba69a32a6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.62.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.62.0":
|
||||
version: 8.62.0
|
||||
resolution: "@typescript-eslint/types@npm:8.62.0"
|
||||
checksum: 10/459f5834dedbb73fc80c8eb92de693faef0f0f341d3b4d65426dbf43640f98a50104f6e15108808aed2c3c66d518db15a648c72c40831f498d36bfb62be564cb
|
||||
"@typescript-eslint/types@npm:8.61.1, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.61.1":
|
||||
version: 8.61.1
|
||||
resolution: "@typescript-eslint/types@npm:8.61.1"
|
||||
checksum: 10/9aa036b27d1874533bf1b931151adaaebb4380cfd14cc71395ab8d2c00c7420218e42811c2b5a68c9fa54cd048e1ab47085afd8b44cd5d736fb5cb8edd300d64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.62.0":
|
||||
version: 8.62.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.62.0"
|
||||
"@typescript-eslint/typescript-estree@npm:8.61.1":
|
||||
version: 8.61.1
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.61.1"
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service": "npm:8.62.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.62.0"
|
||||
"@typescript-eslint/types": "npm:8.62.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.62.0"
|
||||
"@typescript-eslint/project-service": "npm:8.61.1"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.61.1"
|
||||
"@typescript-eslint/types": "npm:8.61.1"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.61.1"
|
||||
debug: "npm:^4.4.3"
|
||||
minimatch: "npm:^10.2.2"
|
||||
semver: "npm:^7.7.3"
|
||||
@@ -5818,32 +5825,32 @@ __metadata:
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/c3ae8e13671957e4a1c4acfc861b40e1545a9d32fe9d5cc851992186314f5a1dbe780cecc8f16b8448a1350f4c11648572352b5d04b77bb62e8dac3e6f3a2e04
|
||||
checksum: 10/36b8b68e7fe9bdba0bb24b46a43a29e39f0cd45440ea190644e230d10e96b0bab9289027668910ff976100d68a9b3bc222618bf96b99bae6fd0053eb560a1257
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.62.0":
|
||||
version: 8.62.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.62.0"
|
||||
"@typescript-eslint/utils@npm:8.61.1":
|
||||
version: 8.61.1
|
||||
resolution: "@typescript-eslint/utils@npm:8.61.1"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.9.1"
|
||||
"@typescript-eslint/scope-manager": "npm:8.62.0"
|
||||
"@typescript-eslint/types": "npm:8.62.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.62.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.61.1"
|
||||
"@typescript-eslint/types": "npm:8.61.1"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.61.1"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/95fed9feb823106f09b517637b0cf00e39fdc3537d05023f84710f23d00457898d8f1c68a69115d624a686411136b5cfdd7b84b657d6b51aea410cf2eb7fde7a
|
||||
checksum: 10/7c8886801f73fc09ecf585b0e8f33799e4a341d51b00db0467f05853f84b808bb98a35e92eab49f28d5d24a1c915959d834214776f0ff6f0cfa5abb3f2e11496
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.62.0":
|
||||
version: 8.62.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.62.0"
|
||||
"@typescript-eslint/visitor-keys@npm:8.61.1":
|
||||
version: 8.61.1
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.61.1"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.62.0"
|
||||
"@typescript-eslint/types": "npm:8.61.1"
|
||||
eslint-visitor-keys: "npm:^5.0.0"
|
||||
checksum: 10/63d66db628befb1d160c02f3fe3b00b7c7ffc47a477335591affde2108e913a72d4a327bdd15d91f5784c3c3624e9b347e9351754390a61ca182bb7e1788d350
|
||||
checksum: 10/7d99c6ae9e91d32b8cc3662ead0e393c912351b5786ece62e1dc198a6b0e9813bb7eae44772970512e7e424a342c86529d9f3dae7c8ab83ac95b5dc33826647a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -6233,13 +6240,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vvo/tzdb@npm:6.198.0":
|
||||
version: 6.198.0
|
||||
resolution: "@vvo/tzdb@npm:6.198.0"
|
||||
checksum: 10/702d25ed7e7a55c4ee3c81e5de79cdb5d11c73bc02e511fa8f93eb497e6ada1b198805469f9e203bef6ea304b914637a6c570f19ade405b6e97d45181a0216a1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@webcomponents/scoped-custom-element-registry@npm:0.0.10":
|
||||
version: 0.0.10
|
||||
resolution: "@webcomponents/scoped-custom-element-registry@npm:0.0.10"
|
||||
@@ -8536,10 +8536,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-plugin-import-x@npm:4.17.0":
|
||||
version: 4.17.0
|
||||
resolution: "eslint-plugin-import-x@npm:4.17.0"
|
||||
"eslint-plugin-import-x@npm:4.16.2":
|
||||
version: 4.16.2
|
||||
resolution: "eslint-plugin-import-x@npm:4.16.2"
|
||||
dependencies:
|
||||
"@package-json/types": "npm:^0.0.12"
|
||||
"@typescript-eslint/types": "npm:^8.56.0"
|
||||
comment-parser: "npm:^1.4.1"
|
||||
debug: "npm:^4.4.1"
|
||||
@@ -8558,7 +8559,7 @@ __metadata:
|
||||
optional: true
|
||||
eslint-import-resolver-node:
|
||||
optional: true
|
||||
checksum: 10/143081e0a2cb418990d5d61c08ad4dd46f4f10dd7664939cc4be8454c2f51cd69134746d2d8b7534786f3a13857d176456bb0c1d1ffc9c168830b4ce93d2c0a8
|
||||
checksum: 10/b149ca51ffba102535cddbe37f298ab3704181ea877a00b87d50062e03ad31a86317cf4205ac3ebc6d4e7872953215777ee4a3b0390616937f3adc9a86b86a90
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9463,10 +9464,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"globals@npm:17.7.0":
|
||||
version: 17.7.0
|
||||
resolution: "globals@npm:17.7.0"
|
||||
checksum: 10/79304ccc4d2ca167ea15bdb25da346aa34ce3847b18fbd6c3cad182e152505305db3c9722fd5e292c62f6db97a8fa06e0c110a1e7703d7325498e5351d08cab4
|
||||
"globals@npm:17.6.0":
|
||||
version: 17.6.0
|
||||
resolution: "globals@npm:17.6.0"
|
||||
checksum: 10/2bf0febf31c942edee6f4eca7e939a9c885f8ecfb767048b1c4dd2a32008d0ab136e6076665d76b44b29c2571bbbc1681371caab05fd8ee0067c7618e841b89d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9510,6 +9511,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"google-timezones-json@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "google-timezones-json@npm:1.2.0"
|
||||
checksum: 10/c83fdaa3681de7b63704aa5d5c644fecd1e2c46047eb65716fd0a1ef28a778b1bbfd6f521c499247b4d7afdc085c7d8bbdbea56398492d395ef9c8d87a648b11
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gopd@npm:^1.0.1, gopd@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "gopd@npm:1.2.0"
|
||||
@@ -9766,7 +9774,6 @@ __metadata:
|
||||
"@types/tar": "npm:7.0.87"
|
||||
"@vibrant/color": "npm:4.0.4"
|
||||
"@vitest/coverage-v8": "npm:4.1.9"
|
||||
"@vvo/tzdb": "npm:6.198.0"
|
||||
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
|
||||
"@webcomponents/webcomponentsjs": "npm:2.8.0"
|
||||
babel-loader: "npm:10.1.1"
|
||||
@@ -9789,7 +9796,7 @@ __metadata:
|
||||
eslint: "npm:10.5.0"
|
||||
eslint-config-prettier: "npm:10.1.8"
|
||||
eslint-import-resolver-webpack: "npm:0.13.11"
|
||||
eslint-plugin-import-x: "npm:4.17.0"
|
||||
eslint-plugin-import-x: "npm:4.16.2"
|
||||
eslint-plugin-lit: "npm:2.3.1"
|
||||
eslint-plugin-lit-a11y: "npm:5.1.1"
|
||||
eslint-plugin-unused-imports: "npm:4.4.1"
|
||||
@@ -9799,7 +9806,8 @@ __metadata:
|
||||
fuse.js: "npm:7.4.2"
|
||||
generate-license-file: "npm:4.2.1"
|
||||
glob: "npm:13.0.6"
|
||||
globals: "npm:17.7.0"
|
||||
globals: "npm:17.6.0"
|
||||
google-timezones-json: "npm:1.2.0"
|
||||
gulp: "npm:5.0.1"
|
||||
gulp-brotli: "npm:3.0.0"
|
||||
gulp-json-transform: "npm:0.5.0"
|
||||
@@ -9849,7 +9857,7 @@ __metadata:
|
||||
tinykeys: "patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch"
|
||||
ts-lit-plugin: "npm:2.0.2"
|
||||
typescript: "npm:6.0.3"
|
||||
typescript-eslint: "npm:8.62.0"
|
||||
typescript-eslint: "npm:8.61.1"
|
||||
vite-tsconfig-paths: "npm:6.1.1"
|
||||
vitest: "npm:4.1.9"
|
||||
webpack-stats-plugin: "npm:1.1.3"
|
||||
@@ -14837,18 +14845,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript-eslint@npm:8.62.0":
|
||||
version: 8.62.0
|
||||
resolution: "typescript-eslint@npm:8.62.0"
|
||||
"typescript-eslint@npm:8.61.1":
|
||||
version: 8.61.1
|
||||
resolution: "typescript-eslint@npm:8.61.1"
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.62.0"
|
||||
"@typescript-eslint/parser": "npm:8.62.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.62.0"
|
||||
"@typescript-eslint/utils": "npm:8.62.0"
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.61.1"
|
||||
"@typescript-eslint/parser": "npm:8.61.1"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.61.1"
|
||||
"@typescript-eslint/utils": "npm:8.61.1"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/c75b16115a3e6f7704f71a9fe14d8cf52129db7e0578755f10a344a0e22474cc0b5383822ef0344cd886b98300af4cce19a306ccda391b2e1eed6c07088f3019
|
||||
checksum: 10/33a798da178f8942a5fb188a991a2eaa9047d7ca95178c67df2565531379b2a587c14ff836716a8b11ed3328c34af5e3b3ea985fa4d2a521a1bb605c2f0e0aa4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user