mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-20 06:57:23 +00:00
Compare commits
1 Commits
copilot/mi
...
split-mode
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dd74f8523 |
12
.github/workflows/ci.yaml
vendored
12
.github/workflows/ci.yaml
vendored
@@ -71,9 +71,12 @@ jobs:
|
||||
- name: Run Tests
|
||||
run: yarn run test
|
||||
build:
|
||||
name: Build frontend
|
||||
name: Build frontend (${{ matrix.target }})
|
||||
needs: [lint, test]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [modern, legacy]
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
@@ -88,10 +91,11 @@ jobs:
|
||||
run: ./node_modules/.bin/gulp build-app
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
BUILD_TARGET: ${{ matrix.target }}
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
name: frontend-bundle-stats-${{ matrix.target }}
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
supervisor:
|
||||
@@ -113,7 +117,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
|
||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
process-only: "issues, prs"
|
||||
|
||||
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
||||
@@ -17,6 +17,18 @@ import {
|
||||
createLandingPageConfig,
|
||||
} from "../rspack.cjs";
|
||||
|
||||
const selectBuildTargets = () => {
|
||||
const target = process.env.BUILD_TARGET?.toLowerCase();
|
||||
switch (target) {
|
||||
case "modern":
|
||||
return [true];
|
||||
case "legacy":
|
||||
return [false];
|
||||
default:
|
||||
return [true, false];
|
||||
}
|
||||
};
|
||||
|
||||
const bothBuilds = (createConfigFunc, params) => [
|
||||
createConfigFunc({ ...params, latestBuild: true }),
|
||||
createConfigFunc({ ...params, latestBuild: false }),
|
||||
@@ -112,12 +124,15 @@ gulp.task("rspack-watch-app", () => {
|
||||
|
||||
gulp.task("rspack-prod-app", () =>
|
||||
prodBuild(
|
||||
bothBuilds(createAppConfig, {
|
||||
selectBuildTargets().map((latestBuild) =>
|
||||
createAppConfig({
|
||||
isProdBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
isTestBuild: env.isTestBuild(),
|
||||
latestBuild,
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("rspack-dev-server-demo", () =>
|
||||
|
||||
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
<div class="action">
|
||||
<span>
|
||||
${this._action
|
||||
? describeAction(this.hass, [], this._action)
|
||||
? describeAction(this.hass, [], [], {}, this._action)
|
||||
: "<invalid YAML>"}
|
||||
</span>
|
||||
<ha-yaml-editor
|
||||
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
${ACTIONS.map(
|
||||
(conf) => html`
|
||||
<div class="action">
|
||||
<span>${describeAction(this.hass, [], conf as any)}</span>
|
||||
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
|
||||
<pre>${dump(conf)}</pre>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.39.4",
|
||||
"@codemirror/view": "6.39.3",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.18.2",
|
||||
@@ -183,7 +183,7 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.1",
|
||||
"eslint": "9.39.2",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -238,6 +238,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.12.0"
|
||||
"node": "22.21.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,36 +17,25 @@ export interface NavigateOptions {
|
||||
// max time to wait for dialogs to close before navigating
|
||||
const DIALOG_WAIT_TIMEOUT = 500;
|
||||
|
||||
/**
|
||||
* Ensures all dialogs are closed before navigation.
|
||||
* Returns true if navigation can proceed, false if a dialog refused to close.
|
||||
*/
|
||||
const ensureDialogsClosed = async (timestamp: number): Promise<boolean> => {
|
||||
export const navigate = async (
|
||||
path: string,
|
||||
options?: NavigateOptions,
|
||||
timestamp = Date.now()
|
||||
) => {
|
||||
const { history } = mainWindow;
|
||||
|
||||
if (!history.state?.dialog || Date.now() - timestamp >= DIALOG_WAIT_TIMEOUT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (history.state?.dialog && Date.now() - timestamp < DIALOG_WAIT_TIMEOUT) {
|
||||
const closed = await closeAllDialogs();
|
||||
if (!closed) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Navigation blocked, because dialog refused to close");
|
||||
return false;
|
||||
}
|
||||
|
||||
// wait for history state to be updated after dialog closed
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve);
|
||||
return new Promise<boolean>((resolve) => {
|
||||
// need to wait for history state to be updated in case a dialog was closed
|
||||
setTimeout(() => {
|
||||
navigate(path, options, timestamp).then(resolve);
|
||||
});
|
||||
});
|
||||
|
||||
return ensureDialogsClosed(timestamp);
|
||||
};
|
||||
|
||||
export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||
const canProceed = await ensureDialogsClosed(Date.now());
|
||||
if (!canProceed) {
|
||||
return false;
|
||||
}
|
||||
const replace = options?.replace || false;
|
||||
|
||||
@@ -79,14 +68,10 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||
* 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());
|
||||
if (!canProceed) {
|
||||
return;
|
||||
}
|
||||
export const goBack = (fallbackPath?: string) => {
|
||||
const { history } = mainWindow;
|
||||
|
||||
// Check if we have history to go back to
|
||||
const { history } = mainWindow;
|
||||
if (history.length > 1) {
|
||||
history.back();
|
||||
return;
|
||||
|
||||
@@ -1,45 +1,30 @@
|
||||
/**
|
||||
* Executes a synchronous callback within a View Transition if supported, otherwise runs it directly.
|
||||
* Executes a callback within a View Transition if supported, otherwise runs it directly.
|
||||
*
|
||||
* @param callback - Synchronous function to execute. The callback will be passed a boolean indicating whether the view transition is available.
|
||||
* @param callback - Function to execute. Can be synchronous or return a Promise. The callback will be passed a boolean indicating whether the view transition is available.
|
||||
* @returns Promise that resolves when the transition completes (or immediately if not supported)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Synchronous callback
|
||||
* withViewTransition(() => {
|
||||
* this.large = !this.large;
|
||||
* });
|
||||
*
|
||||
* // Async callback
|
||||
* await withViewTransition(async () => {
|
||||
* await this.updateData();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export const withViewTransition = (
|
||||
callback: (viewTransitionAvailable: boolean) => void
|
||||
callback: (viewTransitionAvailable: boolean) => void | Promise<void>
|
||||
): Promise<void> => {
|
||||
if (!document.startViewTransition) {
|
||||
callback(false);
|
||||
return Promise.resolve();
|
||||
if (document.startViewTransition) {
|
||||
return document.startViewTransition(() => callback(true)).finished;
|
||||
}
|
||||
|
||||
let callbackInvoked = false;
|
||||
|
||||
try {
|
||||
// View Transitions require DOM updates to happen synchronously within
|
||||
// the callback. Execute the callback immediately (synchronously).
|
||||
const transition = document.startViewTransition(() => {
|
||||
callbackInvoked = true;
|
||||
callback(true);
|
||||
});
|
||||
return transition.finished;
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
"View transition failed, falling back to direct execution.",
|
||||
err
|
||||
);
|
||||
// Make sure the callback is invoked exactly once.
|
||||
if (!callbackInvoked) {
|
||||
callback(false);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
// Fallback: Execute callback directly without transition
|
||||
const result = callback(false);
|
||||
return result instanceof Promise ? result : Promise.resolve();
|
||||
};
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { SunburstSeriesOption } from "echarts/types/dist/echarts";
|
||||
import type { CallbackDataParams } from "echarts/types/src/util/types";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
||||
let SunburstChart: typeof import("echarts/lib/chart/sunburst/install");
|
||||
|
||||
export interface SunburstNode {
|
||||
id: string;
|
||||
name?: string;
|
||||
value: number;
|
||||
itemStyle?: {
|
||||
color?: string;
|
||||
};
|
||||
children?: SunburstNode[];
|
||||
}
|
||||
|
||||
@customElement("ha-sunburst-chart")
|
||||
export class HaSunburstChart extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data?: SunburstNode;
|
||||
|
||||
@property({ type: String, attribute: false }) public valueFormatter?: (
|
||||
value: number
|
||||
) => string;
|
||||
|
||||
public chart?: EChartsType;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (!SunburstChart) {
|
||||
import("echarts/lib/chart/sunburst/install").then((module) => {
|
||||
SunburstChart = module;
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!SunburstChart || !this.data) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const options = {
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: this._renderTooltip,
|
||||
appendTo: document.body,
|
||||
},
|
||||
} as ECOption;
|
||||
|
||||
return html`<ha-chart-base
|
||||
.data=${this._createData(this.data)}
|
||||
.options=${options}
|
||||
height="100%"
|
||||
.extraComponents=${[SunburstChart]}
|
||||
></ha-chart-base>`;
|
||||
}
|
||||
|
||||
private _renderTooltip = (params: CallbackDataParams) => {
|
||||
const data = params.data as { name: string; value: number };
|
||||
const value = this.valueFormatter
|
||||
? this.valueFormatter(data.value)
|
||||
: data.value;
|
||||
return `${params.marker} ${filterXSS(data.name)}<br>${value}`;
|
||||
};
|
||||
|
||||
private _createData = memoizeOne(
|
||||
(data: SunburstNode): SunburstSeriesOption => {
|
||||
const computedStyles = getComputedStyle(this);
|
||||
|
||||
// Transform to echarts format (uses 'name' instead of 'id')
|
||||
const transformNode = (
|
||||
node: SunburstNode,
|
||||
index: number,
|
||||
depth: number,
|
||||
parentColor?: string
|
||||
) => {
|
||||
const result = {
|
||||
...node,
|
||||
name: node.name || node.id,
|
||||
};
|
||||
|
||||
if (depth > 0 && !node.itemStyle?.color) {
|
||||
// Don't assign color to root node
|
||||
result.itemStyle = {
|
||||
color: parentColor ?? getGraphColorByIndex(index, computedStyles),
|
||||
};
|
||||
}
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
result.children = node.children.map((child, i) =>
|
||||
transformNode(child, i, depth + 1, result.itemStyle?.color)
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const transformedData = transformNode(data, 0, 0);
|
||||
|
||||
return {
|
||||
type: "sunburst",
|
||||
data: transformedData.children || [transformedData],
|
||||
radius: [0, "90%"],
|
||||
sort: undefined, // Keep original order
|
||||
label: {
|
||||
show: false,
|
||||
align: "center",
|
||||
rotate: "radial",
|
||||
minAngle: 15,
|
||||
hideOverlap: true,
|
||||
},
|
||||
emphasis: {
|
||||
focus: "ancestor",
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: 2,
|
||||
},
|
||||
levels: this._generateLevels(this._getMaxDepth(data)),
|
||||
} as SunburstSeriesOption;
|
||||
}
|
||||
);
|
||||
|
||||
private _getMaxDepth(node: SunburstNode, currentDepth = 0): number {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return currentDepth;
|
||||
}
|
||||
return Math.max(
|
||||
...node.children.map((child) =>
|
||||
this._getMaxDepth(child, currentDepth + 1)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _generateLevels(depth: number): SunburstSeriesOption["levels"] {
|
||||
const levels: SunburstSeriesOption["levels"] = [];
|
||||
|
||||
// Root level (center) - transparent, small fixed size
|
||||
const rootRadius = 15;
|
||||
const outerRadius = 95;
|
||||
const availableRadius = outerRadius - rootRadius;
|
||||
|
||||
levels.push({
|
||||
r0: "0%",
|
||||
r: `${rootRadius}%`,
|
||||
itemStyle: {
|
||||
color: "transparent",
|
||||
},
|
||||
});
|
||||
|
||||
if (depth === 0) {
|
||||
return levels;
|
||||
}
|
||||
|
||||
// Distribute remaining radius among data levels using weighted distribution
|
||||
// First level gets most space, each subsequent level gets progressively smaller
|
||||
const weights = Array.from({ length: depth }, (_, i) => depth - i);
|
||||
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
|
||||
|
||||
let currentRadius = rootRadius;
|
||||
for (let i = 0; i < depth; i++) {
|
||||
const levelRadius = (weights[i] / totalWeight) * availableRadius;
|
||||
const r0 = currentRadius;
|
||||
const r = currentRadius + levelRadius;
|
||||
currentRadius = r;
|
||||
|
||||
levels.push({
|
||||
r0: `${r0}%`,
|
||||
r: `${r}%`,
|
||||
// Show labels only on first level
|
||||
...(i === 0 ? { label: { show: true } } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
return levels;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
flex: 1;
|
||||
}
|
||||
ha-chart-base {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-sunburst-chart": HaSunburstChart;
|
||||
}
|
||||
}
|
||||
@@ -184,11 +184,17 @@ export class StatisticsChart extends LitElement {
|
||||
}
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
if (!this._legendData) {
|
||||
return;
|
||||
}
|
||||
this._hiddenStats.add(ev.detail.id);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
}
|
||||
|
||||
private _datasetUnhidden(ev: CustomEvent) {
|
||||
if (!this._legendData) {
|
||||
return;
|
||||
}
|
||||
this._hiddenStats.delete(ev.detail.id);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
}
|
||||
@@ -515,9 +521,7 @@ export class StatisticsChart extends LitElement {
|
||||
`ui.components.statistics_charts.statistic_types.${type}`
|
||||
),
|
||||
symbol: "none",
|
||||
// minmax sampling operates independently per series, breaking stacking alignment
|
||||
// https://github.com/apache/echarts/issues/11879
|
||||
sampling: band && drawBands ? "lttb" : "minmax",
|
||||
sampling: "minmax",
|
||||
animationDurationUpdate: 0,
|
||||
lineStyle: {
|
||||
width: 1.5,
|
||||
@@ -535,19 +539,12 @@ export class StatisticsChart extends LitElement {
|
||||
if (band && this.chartType === "line") {
|
||||
series.stack = `band-${statistic_id}`;
|
||||
series.stackStrategy = "all";
|
||||
if (this._hiddenStats.has(`${statistic_id}-${bandBottom}`)) {
|
||||
// changing the stackOrder forces echarts to render the stacked series that are not hidden #28472
|
||||
series.stackOrder = "seriesDesc";
|
||||
(series as LineSeriesOption).areaStyle = undefined;
|
||||
} else {
|
||||
series.stackOrder = "seriesAsc";
|
||||
if (drawBands && type === bandTop) {
|
||||
(series as LineSeriesOption).areaStyle = {
|
||||
color: color + "3F",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.hideLegend) {
|
||||
const showLegend = hasMean
|
||||
? type === "mean"
|
||||
@@ -589,8 +586,7 @@ export class StatisticsChart extends LitElement {
|
||||
} else if (
|
||||
type === bandTop &&
|
||||
this.chartType === "line" &&
|
||||
drawBands &&
|
||||
!this._hiddenStats.has(`${statistic_id}-${bandBottom}`)
|
||||
drawBands
|
||||
) {
|
||||
const top = stat[bandTop] || 0;
|
||||
val.push(Math.abs(top - (stat[bandBottom] || 0)));
|
||||
|
||||
@@ -1402,9 +1402,6 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.scroller {
|
||||
height: calc(100% - 57px);
|
||||
|
||||
@@ -101,10 +101,6 @@ const Component = Vue.extend({
|
||||
type: String,
|
||||
default: "en",
|
||||
},
|
||||
opensVertical: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
// @ts-expect-error
|
||||
@@ -133,11 +129,6 @@ const Component = Vue.extend({
|
||||
},
|
||||
expression: "dateRange",
|
||||
},
|
||||
on: {
|
||||
toggle: (open: boolean) => {
|
||||
fireEvent(this.$el as HTMLElement, "toggle", { open });
|
||||
},
|
||||
},
|
||||
scopedSlots: {
|
||||
input() {
|
||||
return createElement("slot", {
|
||||
@@ -318,10 +309,6 @@ class DateRangePickerElement extends WrappedElement {
|
||||
min-width: unset !important;
|
||||
display: block !important;
|
||||
}
|
||||
:host([opens-vertical="up"]) .daterangepicker {
|
||||
bottom: 100%;
|
||||
top: auto !important;
|
||||
}
|
||||
`;
|
||||
if (mainWindow.document.dir === "rtl") {
|
||||
style.innerHTML += `
|
||||
@@ -353,7 +340,4 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"date-range-picker": DateRangePickerElement;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
toggle: { open: boolean };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,8 +205,6 @@ export class HaDevicePicker extends LitElement {
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
.label=${this.label}
|
||||
.searchLabel=${this.searchLabel}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
|
||||
@@ -472,7 +472,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
.autofocus=${this.autofocus}
|
||||
.allowCustomValue=${this.allowCustomEntity}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.statistic-picker.no_statistics"
|
||||
|
||||
@@ -52,10 +52,8 @@ export class HaAutomationRow extends LitElement {
|
||||
<slot name="leading-icon"></slot>
|
||||
</div>
|
||||
<slot class="header" name="header"></slot>
|
||||
<div class="icons">
|
||||
<slot name="icons"></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -120,11 +118,12 @@ export class HaAutomationRow extends LitElement {
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
padding: 0 var(--ha-space-3);
|
||||
padding: var(--ha-space-0) var(--ha-space-2);
|
||||
min-height: 48px;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
outline: none;
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
}
|
||||
@@ -141,15 +140,11 @@ export class HaAutomationRow extends LitElement {
|
||||
background-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
padding: var(--ha-space-1);
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leading-icon-wrapper {
|
||||
padding-top: var(--ha-space-3);
|
||||
}
|
||||
::slotted([slot="leading-icon"]) {
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
@@ -177,10 +172,6 @@ export class HaAutomationRow extends LitElement {
|
||||
overflow-wrap: anywhere;
|
||||
margin: var(--ha-space-0) var(--ha-space-3);
|
||||
}
|
||||
.icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
:host([sort-selected]) .row {
|
||||
outline: solid;
|
||||
outline-color: rgba(var(--rgb-accent-color), 0.6);
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { ConfigEntry } from "../data/config_entries";
|
||||
import { getConfigEntries } from "../data/config_entries";
|
||||
import { domainToName } from "../data/integration";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { ValueChangedEvent, HomeAssistant } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "primary", weight: 10 },
|
||||
{ name: "secondary", weight: 8 },
|
||||
{ name: "icon", weight: 5 },
|
||||
];
|
||||
export interface ConfigEntryExtended extends ConfigEntry {
|
||||
localized_domain_name?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-config-entry-picker")
|
||||
class HaConfigEntryPicker extends LitElement {
|
||||
@@ -30,107 +28,119 @@ class HaConfigEntryPicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@state() private _configEntries?: PickerComboBoxItem[];
|
||||
@state() private _configEntries?: ConfigEntryExtended[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@query("ha-generic-picker") private _picker!: HaGenericPicker;
|
||||
@query("ha-combo-box") private _comboBox!: HaComboBox;
|
||||
|
||||
public open() {
|
||||
this._picker?.open();
|
||||
this._comboBox?.open();
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this._picker?.focus();
|
||||
this._comboBox?.focus();
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._getConfigEntries();
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<ConfigEntryExtended> = (
|
||||
item
|
||||
) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<span slot="headline">
|
||||
${item.title ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.unnamed_entry"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">${item.localized_domain_name}</span>
|
||||
<img
|
||||
alt=""
|
||||
slot="start"
|
||||
src=${brandsUrl({
|
||||
domain: item.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
protected render() {
|
||||
if (!this._configEntries) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.placeholder=${this.label === undefined && this.hass
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.config-entry-picker.config_entry")
|
||||
: this.label}
|
||||
show-label
|
||||
.value=${this.value}
|
||||
.value=${this._value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.getItems=${this._getItems}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.renderer=${this._rowRenderer}
|
||||
.items=${this._configEntries}
|
||||
item-value-path="entry_id"
|
||||
item-id-path="entry_id"
|
||||
item-label-path="title"
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-generic-picker>
|
||||
></ha-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
private _rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<span slot="headline">${item.primary}</span>
|
||||
<span slot="supporting-text">${item.secondary}</span>
|
||||
<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${item.icon!}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
private _onImageLoad(ev) {
|
||||
ev.target.style.visibility = "initial";
|
||||
}
|
||||
|
||||
private _onImageError(ev) {
|
||||
ev.target.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
private async _getConfigEntries() {
|
||||
getConfigEntries(this.hass, {
|
||||
type: ["device", "hub", "service"],
|
||||
domain: this.integration,
|
||||
}).then((configEntries) => {
|
||||
this._configEntries = configEntries.map((entry: ConfigEntry) => {
|
||||
const domainName = domainToName(this.hass.localize, entry.domain);
|
||||
return {
|
||||
id: entry.entry_id,
|
||||
icon: entry.domain,
|
||||
primary:
|
||||
entry.title ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.unnamed_entry"
|
||||
this._configEntries = configEntries
|
||||
.map(
|
||||
(entry: ConfigEntry): ConfigEntryExtended => ({
|
||||
...entry,
|
||||
localized_domain_name: domainToName(
|
||||
this.hass.localize,
|
||||
entry.domain
|
||||
),
|
||||
secondary: domainName,
|
||||
sorting_label: [entry.title, domainName].filter(Boolean).join("_"),
|
||||
};
|
||||
});
|
||||
})
|
||||
)
|
||||
.sort((conf1, conf2) =>
|
||||
caseInsensitiveStringCompare(
|
||||
conf1.localized_domain_name + conf1.title,
|
||||
conf2.localized_domain_name + conf2.title,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _valueRenderer = (itemId: string) => {
|
||||
const item = this._configEntries!.find((entry) => entry.id === itemId);
|
||||
return html`<span
|
||||
style="display: flex; align-items: center; gap: var(--ha-space-2);"
|
||||
slot="headline"
|
||||
>${item?.icon
|
||||
? html`<ha-domain-icon
|
||||
.hass=${this.hass}
|
||||
.domain=${item.icon!}
|
||||
brand-fallback
|
||||
></ha-domain-icon>`
|
||||
: nothing}${item?.primary || "Unknown"}</span
|
||||
>`;
|
||||
};
|
||||
|
||||
private _getItems = () => this._configEntries!;
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (newValue !== this.value) {
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,9 +74,6 @@ export class HaDateRangePicker extends LitElement {
|
||||
@property({ attribute: "extended-presets", type: Boolean })
|
||||
public extendedPresets = false;
|
||||
|
||||
@property({ attribute: "vertical-opening-direction" })
|
||||
public verticalOpeningDirection?: "up" | "down";
|
||||
|
||||
@property({ attribute: false }) public openingDirection?:
|
||||
| "right"
|
||||
| "left"
|
||||
@@ -130,7 +127,6 @@ export class HaDateRangePicker extends LitElement {
|
||||
opening-direction=${ifDefined(
|
||||
this.openingDirection || this._calcedOpeningDirection
|
||||
)}
|
||||
opens-vertical=${ifDefined(this.verticalOpeningDirection)}
|
||||
first-day=${firstWeekdayIndex(this.hass.locale)}
|
||||
language=${this.hass.locale.language}
|
||||
@change=${this._handleChange}
|
||||
|
||||
@@ -47,7 +47,6 @@ export class HaDomainIcon extends LitElement {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
|
||||
return this._renderFallback();
|
||||
});
|
||||
|
||||
|
||||
@@ -390,8 +390,6 @@ export class HaFloorPicker extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.floor-picker.no_floors"
|
||||
|
||||
@@ -34,12 +34,10 @@ export class HaGenericPicker extends LitElement {
|
||||
@property({ type: Boolean, attribute: "allow-custom-value" })
|
||||
public allowCustomValue;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
@@ -142,10 +140,6 @@ export class HaGenericPicker extends LitElement {
|
||||
// helper to set new value after closing picker, to avoid flicker
|
||||
private _newValue?: string;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public invalid = false;
|
||||
|
||||
private _unsubscribeTinyKeys?: () => void;
|
||||
|
||||
protected render() {
|
||||
@@ -178,15 +172,13 @@ export class HaGenericPicker extends LitElement {
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@click=${this.open}
|
||||
@clear=${this._clear}
|
||||
.icon=${this.icon}
|
||||
.showLabel=${this.showLabel}
|
||||
.placeholder=${this.placeholder}
|
||||
.showLabel=${this.showLabel}
|
||||
.value=${this.value}
|
||||
.valueRenderer=${this.valueRenderer}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.invalid=${this.invalid}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.valueRenderer=${this.valueRenderer}
|
||||
>
|
||||
</ha-picker-field>`}
|
||||
</slot>
|
||||
@@ -269,16 +261,11 @@ export class HaGenericPicker extends LitElement {
|
||||
);
|
||||
|
||||
private _renderHelper() {
|
||||
const showError = this.invalid && this.errorMessage;
|
||||
const showHelper = !showError && this.helper;
|
||||
|
||||
if (!showError && !showHelper) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${showError ? this.errorMessage : this.helper}
|
||||
</ha-input-helper-text>`;
|
||||
return this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _dialogOpened = () => {
|
||||
@@ -377,9 +364,6 @@ export class HaGenericPicker extends LitElement {
|
||||
display: block;
|
||||
margin: var(--ha-space-2) 0 0;
|
||||
}
|
||||
:host([invalid]) ha-input-helper-text {
|
||||
color: var(--mdc-theme-error, var(--error-color, #b00020));
|
||||
}
|
||||
|
||||
wa-popover {
|
||||
--wa-space-l: var(--ha-space-0);
|
||||
@@ -402,6 +386,12 @@ export class HaGenericPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 1000px) {
|
||||
wa-popover::part(body) {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-bottom-sheet {
|
||||
--ha-bottom-sheet-height: 90vh;
|
||||
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type {
|
||||
ComboBoxDataProviderCallback,
|
||||
ComboBoxDataProviderParams,
|
||||
} from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -6,54 +10,35 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { customIcons } from "../data/custom_icons";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-generic-picker";
|
||||
import "./ha-combo-box";
|
||||
import "./ha-icon";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
|
||||
interface IconItem {
|
||||
icon: string;
|
||||
parts: Set<string>;
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
interface RankedIcon {
|
||||
item: PickerComboBoxItem;
|
||||
icon: string;
|
||||
rank: number;
|
||||
}
|
||||
|
||||
let ICONS: PickerComboBoxItem[] = [];
|
||||
let ICONS: IconItem[] = [];
|
||||
let ICONS_LOADED = false;
|
||||
|
||||
interface IconData {
|
||||
name: string;
|
||||
keywords?: string[];
|
||||
}
|
||||
|
||||
const createIconItem = (icon: IconData, prefix: string): PickerComboBoxItem => {
|
||||
const iconId = `${prefix}:${icon.name}`;
|
||||
const iconName = icon.name;
|
||||
const parts = iconName.split("-");
|
||||
const keywords = icon.keywords ?? [];
|
||||
const searchLabels: Record<string, string> = {
|
||||
iconName,
|
||||
};
|
||||
parts.forEach((part, index) => {
|
||||
searchLabels[`part${index}`] = part;
|
||||
});
|
||||
keywords.forEach((keyword, index) => {
|
||||
searchLabels[`keyword${index}`] = keyword;
|
||||
});
|
||||
return {
|
||||
id: iconId,
|
||||
primary: iconId,
|
||||
icon: iconId,
|
||||
search_labels: searchLabels,
|
||||
sorting_label: iconId,
|
||||
};
|
||||
};
|
||||
|
||||
const loadIcons = async () => {
|
||||
ICONS_LOADED = true;
|
||||
|
||||
const iconList = await import("../../build/mdi/iconList.json");
|
||||
ICONS = iconList.default.map((icon) => createIconItem(icon, "mdi"));
|
||||
ICONS = iconList.default.map((icon) => ({
|
||||
icon: `mdi:${icon.name}`,
|
||||
parts: new Set(icon.name.split("-")),
|
||||
keywords: icon.keywords,
|
||||
}));
|
||||
|
||||
const customIconLoads: Promise<PickerComboBoxItem[]>[] = [];
|
||||
const customIconLoads: Promise<IconItem[]>[] = [];
|
||||
Object.keys(customIcons).forEach((iconSet) => {
|
||||
customIconLoads.push(loadCustomIconItems(iconSet));
|
||||
});
|
||||
@@ -62,16 +47,19 @@ const loadIcons = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const loadCustomIconItems = async (
|
||||
iconsetPrefix: string
|
||||
): Promise<PickerComboBoxItem[]> => {
|
||||
const loadCustomIconItems = async (iconsetPrefix: string) => {
|
||||
try {
|
||||
const getIconList = customIcons[iconsetPrefix].getIconList;
|
||||
if (typeof getIconList !== "function") {
|
||||
return [];
|
||||
}
|
||||
const iconList = await getIconList();
|
||||
return iconList.map((icon) => createIconItem(icon, iconsetPrefix));
|
||||
const customIconItems = iconList.map((icon) => ({
|
||||
icon: `${iconsetPrefix}:${icon.name}`,
|
||||
parts: new Set(icon.name.split("-")),
|
||||
keywords: icon.keywords ?? [],
|
||||
}));
|
||||
return customIconItems;
|
||||
} catch (_err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Unable to load icon list for ${iconsetPrefix} iconset`);
|
||||
@@ -79,10 +67,10 @@ const loadCustomIconItems = async (
|
||||
}
|
||||
};
|
||||
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<ha-icon .icon=${item.id} slot="start"></ha-icon>
|
||||
${item.id}
|
||||
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
|
||||
${item.icon}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
@@ -106,99 +94,85 @@ export class HaIconPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public invalid = false;
|
||||
|
||||
private _getIconPickerItems = (): PickerComboBoxItem[] => ICONS;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
item-value-path="icon"
|
||||
item-label-path="icon"
|
||||
.value=${this._value}
|
||||
allow-custom-value
|
||||
show-label
|
||||
.getItems=${this._getIconPickerItems}
|
||||
.dataProvider=${ICONS_LOADED ? this._iconProvider : undefined}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.placeholder=${this.placeholder}
|
||||
.errorMessage=${this.errorMessage}
|
||||
.invalid=${this.invalid}
|
||||
.rowRenderer=${rowRenderer}
|
||||
.icon=${this._icon}
|
||||
.placeholder=${this.label}
|
||||
.value=${this._value}
|
||||
.searchFn=${this._filterIcons}
|
||||
.notFoundLabel=${this.hass?.localize(
|
||||
"ui.components.icon-picker.no_match"
|
||||
)}
|
||||
popover-placement="bottom-start"
|
||||
.renderer=${rowRenderer}
|
||||
icon
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
${this._value || this.placeholder
|
||||
? html`
|
||||
<ha-icon .icon=${this._value || this.placeholder} slot="icon">
|
||||
</ha-icon>
|
||||
`
|
||||
: html`<slot slot="icon" name="fallback"></slot>`}
|
||||
</ha-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
// Filter can take a significant chunk of frame (up to 3-5 ms)
|
||||
private _filterIcons = memoizeOne(
|
||||
(
|
||||
filter: string,
|
||||
filteredItems: PickerComboBoxItem[],
|
||||
allItems: PickerComboBoxItem[]
|
||||
): PickerComboBoxItem[] => {
|
||||
const normalizedFilter = filter.toLowerCase().replace(/\s+/g, "-");
|
||||
const iconItems = allItems?.length ? allItems : filteredItems;
|
||||
|
||||
if (!normalizedFilter.length) {
|
||||
(filter: string, iconItems: IconItem[] = ICONS) => {
|
||||
if (!filter) {
|
||||
return iconItems;
|
||||
}
|
||||
|
||||
const rankedItems: RankedIcon[] = [];
|
||||
const filteredItems: RankedIcon[] = [];
|
||||
const addIcon = (icon: string, rank: number) =>
|
||||
filteredItems.push({ icon, rank });
|
||||
|
||||
// Filter and rank such that exact matches rank higher, and prefer icon name matches over keywords
|
||||
for (const item of iconItems) {
|
||||
const iconName = (item.id.split(":")[1] || item.id).toLowerCase();
|
||||
const parts = iconName.split("-");
|
||||
const keywords = item.search_labels
|
||||
? Object.values(item.search_labels)
|
||||
.filter((v): v is string => v !== null)
|
||||
.map((v) => v.toLowerCase())
|
||||
: [];
|
||||
const id = item.id.toLowerCase();
|
||||
|
||||
if (parts.includes(normalizedFilter)) {
|
||||
rankedItems.push({ item, rank: 1 });
|
||||
} else if (keywords.includes(normalizedFilter)) {
|
||||
rankedItems.push({ item, rank: 2 });
|
||||
} else if (id.includes(normalizedFilter)) {
|
||||
rankedItems.push({ item, rank: 3 });
|
||||
} else if (keywords.some((word) => word.includes(normalizedFilter))) {
|
||||
rankedItems.push({ item, rank: 4 });
|
||||
if (item.parts.has(filter)) {
|
||||
addIcon(item.icon, 1);
|
||||
} else if (item.keywords.includes(filter)) {
|
||||
addIcon(item.icon, 2);
|
||||
} else if (item.icon.includes(filter)) {
|
||||
addIcon(item.icon, 3);
|
||||
} else if (item.keywords.some((word) => word.includes(filter))) {
|
||||
addIcon(item.icon, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow preview for custom icon not in list
|
||||
if (rankedItems.length === 0) {
|
||||
rankedItems.push({
|
||||
item: {
|
||||
id: filter,
|
||||
primary: filter,
|
||||
icon: filter,
|
||||
search_labels: { keyword: filter },
|
||||
sorting_label: filter,
|
||||
},
|
||||
rank: 0,
|
||||
});
|
||||
if (filteredItems.length === 0) {
|
||||
addIcon(filter, 0);
|
||||
}
|
||||
|
||||
return rankedItems
|
||||
.sort((itemA, itemB) => itemA.rank - itemB.rank)
|
||||
.map((item) => item.item);
|
||||
return filteredItems.sort((itemA, itemB) => itemA.rank - itemB.rank);
|
||||
}
|
||||
);
|
||||
|
||||
protected firstUpdated() {
|
||||
if (!ICONS_LOADED) {
|
||||
loadIcons().then(() => {
|
||||
this._getIconPickerItems = (): PickerComboBoxItem[] => ICONS;
|
||||
private _iconProvider = (
|
||||
params: ComboBoxDataProviderParams,
|
||||
callback: ComboBoxDataProviderCallback<IconItem | RankedIcon>
|
||||
) => {
|
||||
const filteredItems = this._filterIcons(params.filter.toLowerCase(), ICONS);
|
||||
const iStart = params.page * params.pageSize;
|
||||
const iEnd = iStart + params.pageSize;
|
||||
callback(filteredItems.slice(iStart, iEnd), filteredItems.length);
|
||||
};
|
||||
|
||||
private async _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
const opened = ev.detail.value;
|
||||
if (opened && !ICONS_LOADED) {
|
||||
await loadIcons();
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,18 +194,20 @@ export class HaIconPicker extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private get _icon() {
|
||||
return this.value?.length ? this.value : this.placeholder;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-generic-picker {
|
||||
width: 100%;
|
||||
display: block;
|
||||
*[slot="icon"] {
|
||||
color: var(--primary-text-color);
|
||||
position: relative;
|
||||
bottom: 2px;
|
||||
}
|
||||
*[slot="prefix"] {
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.label-picker.no_labels"
|
||||
|
||||
@@ -15,7 +15,6 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
import type { HaComboBoxItem } from "./ha-combo-box-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-icon";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -33,8 +32,6 @@ export class HaPickerField extends LitElement {
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
@@ -52,8 +49,6 @@ export class HaPickerField extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public valueRenderer?: PickerValueRenderer;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public invalid = false;
|
||||
|
||||
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
|
||||
|
||||
@state()
|
||||
@@ -66,32 +61,23 @@ export class HaPickerField extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const hasValue = !!this.value?.length;
|
||||
|
||||
const showClearIcon =
|
||||
!!this.value && !this.required && !this.disabled && !this.hideClearIcon;
|
||||
|
||||
const overlineLabel =
|
||||
this.showLabel && hasValue && this.placeholder
|
||||
const placeholder = this.showLabel
|
||||
? html`<span slot="overline">${this.placeholder}</span>`
|
||||
: nothing;
|
||||
|
||||
const headlineContent = hasValue
|
||||
? this.valueRenderer
|
||||
? this.valueRenderer(this.value ?? "")
|
||||
: html`<span slot="headline">${this.value}</span>`
|
||||
: this.placeholder
|
||||
? html`<span slot="headline" class="placeholder">
|
||||
${this.placeholder}
|
||||
</span>`
|
||||
: nothing;
|
||||
|
||||
return html`
|
||||
<ha-combo-box-item .disabled=${this.disabled} type="button" compact>
|
||||
${this.icon
|
||||
? html`<ha-icon slot="start" .icon=${this.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${overlineLabel}${headlineContent}
|
||||
${this.value
|
||||
? this.valueRenderer
|
||||
? html`${placeholder}${this.valueRenderer(this.value)}`
|
||||
: html`${placeholder}<span slot="headline">${this.value}</span>`
|
||||
: html`
|
||||
<span slot="headline" class="placeholder">
|
||||
${this.placeholder}
|
||||
</span>
|
||||
`}
|
||||
${this.unknown
|
||||
? html`<div slot="supporting-text" class="unknown">
|
||||
${this.unknownItemText ||
|
||||
@@ -183,11 +169,6 @@ export class HaPickerField extends LitElement {
|
||||
background-color: var(--ha-color-fill-warning-quiet-resting);
|
||||
}
|
||||
|
||||
:host([invalid]) ha-combo-box-item:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-error, var(--error-color, #b00020));
|
||||
}
|
||||
|
||||
.clear {
|
||||
margin: 0 -8px;
|
||||
--mdc-icon-button-size: 32px;
|
||||
|
||||
@@ -36,7 +36,6 @@ export class HaLabelSelector extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
</ha-labels-picker>
|
||||
@@ -50,7 +49,6 @@ export class HaLabelSelector extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
</ha-label-picker>
|
||||
|
||||
@@ -68,8 +68,8 @@ class HaEntityMarker extends LitElement {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
width: var(--ha-marker-size, 48px);
|
||||
height: var(--ha-marker-size, 48px);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: var(--ha-marker-font-size, var(--ha-font-size-xl));
|
||||
border-radius: var(--ha-marker-border-radius, 50%);
|
||||
border: 1px solid var(--ha-marker-color, var(--primary-color));
|
||||
|
||||
@@ -560,11 +560,10 @@ export class HaMap extends ReactiveElement {
|
||||
radius,
|
||||
});
|
||||
|
||||
const markerIconSize = this._getMarkerSize(computedStyles) / 2;
|
||||
const marker = new DecoratedMarker([latitude, longitude], circle, {
|
||||
icon: Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [markerIconSize, markerIconSize],
|
||||
iconSize: [24, 24],
|
||||
className,
|
||||
}),
|
||||
interactive: this.interactiveZones,
|
||||
@@ -619,11 +618,10 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
|
||||
// create marker with the icon
|
||||
const markerSize = this._getMarkerSize(computedStyles);
|
||||
const marker = new DecoratedMarker([latitude, longitude], undefined, {
|
||||
icon: Leaflet.divIcon({
|
||||
html: entityMarker,
|
||||
iconSize: [markerSize, markerSize],
|
||||
iconSize: [48, 48],
|
||||
className: "",
|
||||
}),
|
||||
title: title,
|
||||
@@ -659,13 +657,6 @@ export class HaMap extends ReactiveElement {
|
||||
this._mapZones.forEach((marker) => map.addLayer(marker));
|
||||
}
|
||||
|
||||
private _getMarkerSize(computedStyles: CSSStyleDeclaration): number {
|
||||
const markerSizeVarValue =
|
||||
computedStyles.getPropertyValue("--ha-marker-size");
|
||||
const parsed = parseFloat(markerSizeVarValue);
|
||||
return Number.isNaN(parsed) ? 48 : parsed;
|
||||
}
|
||||
|
||||
private async _attachObserver(): Promise<void> {
|
||||
if (!this._resizeObserver) {
|
||||
this._resizeObserver = new ResizeObserver(() => {
|
||||
@@ -748,19 +739,14 @@ export class HaMap extends ReactiveElement {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
--mdc-icon-size: calc(var(--ha-marker-size, 48px) / 2);
|
||||
}
|
||||
|
||||
.marker-cluster div {
|
||||
background-clip: padding-box;
|
||||
background-color: var(--primary-color);
|
||||
border: 3px solid rgba(var(--rgb-primary-color), 0.2);
|
||||
width: calc(var(--ha-marker-size, 48px) * 0.667);
|
||||
height: calc(var(--ha-marker-size, 48px) * 0.667);
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--ha-border-radius-2xl);
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
color: var(--text-primary-color);
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,13 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import { describeCondition, describeTrigger } from "../../data/automation_i18n";
|
||||
import { fullEntitiesContext, labelsContext } from "../../data/context";
|
||||
import {
|
||||
floorsContext,
|
||||
fullEntitiesContext,
|
||||
labelsContext,
|
||||
} from "../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import type { LabelRegistryEntry } from "../../data/label/label_registry";
|
||||
import type { LogbookEntry } from "../../data/logbook";
|
||||
import { describeAction } from "../../data/script_i18n";
|
||||
@@ -58,6 +63,10 @@ export class HaTracePathDetails extends LitElement {
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelReg!: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
_floorReg!: Record<string, FloorRegistryEntry>;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="padded-box trace-info">
|
||||
@@ -184,6 +193,8 @@ export class HaTracePathDetails extends LitElement {
|
||||
${describeAction(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
currentDetail
|
||||
)}
|
||||
</h2>`
|
||||
|
||||
@@ -15,8 +15,14 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
|
||||
import { relativeTime } from "../../common/datetime/relative_time";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import {
|
||||
floorsContext,
|
||||
fullEntitiesContext,
|
||||
labelsContext,
|
||||
} from "../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import type { LabelRegistryEntry } from "../../data/label/label_registry";
|
||||
import type { LogbookEntry } from "../../data/logbook";
|
||||
import type {
|
||||
ChooseAction,
|
||||
@@ -191,6 +197,8 @@ class ActionRenderer {
|
||||
constructor(
|
||||
private hass: HomeAssistant,
|
||||
private entityReg: EntityRegistryEntry[],
|
||||
private labelReg: LabelRegistryEntry[],
|
||||
private floorReg: Record<string, FloorRegistryEntry>,
|
||||
private entries: TemplateResult[],
|
||||
private trace: AutomationTraceExtended,
|
||||
private logbookRenderer: LogbookRenderer,
|
||||
@@ -305,7 +313,14 @@ class ActionRenderer {
|
||||
|
||||
this._renderEntry(
|
||||
path,
|
||||
describeAction(this.hass, this.entityReg, data, actionType),
|
||||
describeAction(
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
data,
|
||||
actionType
|
||||
),
|
||||
undefined,
|
||||
data.enabled === false
|
||||
);
|
||||
@@ -470,7 +485,13 @@ class ActionRenderer {
|
||||
|
||||
const name =
|
||||
repeatConfig.alias ||
|
||||
describeAction(this.hass, this.entityReg, repeatConfig);
|
||||
describeAction(
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
repeatConfig
|
||||
);
|
||||
|
||||
this._renderEntry(repeatPath, name, undefined, disabled);
|
||||
|
||||
@@ -564,7 +585,14 @@ class ActionRenderer {
|
||||
this._renderEntry(
|
||||
sequencePath,
|
||||
sequenceConfig.alias ||
|
||||
describeAction(this.hass, this.entityReg, sequenceConfig, "sequence"),
|
||||
describeAction(
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
sequenceConfig,
|
||||
"sequence"
|
||||
),
|
||||
undefined,
|
||||
sequenceConfig.enabled === false
|
||||
);
|
||||
@@ -655,6 +683,14 @@ export class HaAutomationTracer extends LitElement {
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelReg!: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
_floorReg!: Record<string, FloorRegistryEntry>;
|
||||
|
||||
protected render() {
|
||||
if (!this.trace) {
|
||||
return nothing;
|
||||
@@ -671,6 +707,8 @@ export class HaAutomationTracer extends LitElement {
|
||||
const actionRenderer = new ActionRenderer(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
entries,
|
||||
this.trace,
|
||||
logbookRenderer,
|
||||
|
||||
@@ -1363,9 +1363,9 @@ export const calculateSolarConsumedGauge = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current power value from entity state, normalized to watts (W)
|
||||
* Get current power value from entity state, normalized to kW
|
||||
* @param stateObj - The entity state object to get power value from
|
||||
* @returns Power value in W (watts), or undefined if entity not found or invalid
|
||||
* @returns Power value in kW, or 0 if entity not found or invalid
|
||||
*/
|
||||
export const getPowerFromState = (stateObj: HassEntity): number | undefined => {
|
||||
if (!stateObj) {
|
||||
@@ -1376,54 +1376,22 @@ export const getPowerFromState = (stateObj: HassEntity): number | undefined => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Normalize to watts (W) based on unit of measurement (case-sensitive)
|
||||
// Normalize to kW based on unit of measurement (case-sensitive)
|
||||
// Supported units: GW, kW, MW, mW, TW, W
|
||||
const unit = stateObj.attributes.unit_of_measurement;
|
||||
switch (unit) {
|
||||
case "W":
|
||||
return value;
|
||||
case "kW":
|
||||
return value * 1000;
|
||||
case "mW":
|
||||
return value / 1000;
|
||||
case "mW":
|
||||
return value / 1000000;
|
||||
case "MW":
|
||||
return value * 1_000_000;
|
||||
return value * 1000;
|
||||
case "GW":
|
||||
return value * 1_000_000_000;
|
||||
return value * 1000000;
|
||||
case "TW":
|
||||
return value * 1_000_000_000_000;
|
||||
return value * 1000000000;
|
||||
default:
|
||||
// Assume value is in watts (W) if no unit or an unsupported unit is provided
|
||||
// Assume kW if no unit or unit is kW
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Format power value in watts (W) to a short string with the appropriate unit
|
||||
* @param hass - The HomeAssistant instance
|
||||
* @param powerWatts - The power value in watts (W)
|
||||
* @returns A string with the formatted power value and unit
|
||||
*/
|
||||
export const formatPowerShort = (
|
||||
hass: HomeAssistant,
|
||||
powerWatts: number
|
||||
): string => {
|
||||
const units = ["W", "kW", "MW", "GW", "TW"];
|
||||
let unitIndex = 0;
|
||||
let value = powerWatts;
|
||||
|
||||
// Scale the unit to the appropriate power of 1000
|
||||
while (Math.abs(value) >= 1000 && unitIndex < units.length - 1) {
|
||||
value /= 1000;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return (
|
||||
formatNumber(value, hass.locale, {
|
||||
// For watts, show no decimals. For kW and above, always show 3 decimals.
|
||||
maximumFractionDigits: units[unitIndex] === "W" ? 0 : 3,
|
||||
}) +
|
||||
" " +
|
||||
units[unitIndex]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -196,7 +196,6 @@ export const fetchHostDisksUsage = async (hass: HomeAssistant) => {
|
||||
endpoint: "/host/disks/default/usage",
|
||||
method: "get",
|
||||
timeout: 3600, // seconds. This can take a while
|
||||
params: { max_depth: 3 },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Condition } from "../../../panels/lovelace/common/validate-condition";
|
||||
import type { MediaSelectorValue } from "../../selector";
|
||||
import type { LovelaceBadgeConfig } from "./badge";
|
||||
import type { LovelaceCardConfig } from "./card";
|
||||
@@ -41,7 +40,6 @@ export interface LovelaceViewSidebarConfig {
|
||||
sections?: LovelaceSectionConfig[];
|
||||
content_label?: string;
|
||||
sidebar_label?: string;
|
||||
visibility?: Condition[];
|
||||
}
|
||||
|
||||
export interface LovelaceBaseViewConfig {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { formatNumericDuration } from "../common/datetime/format_duration";
|
||||
import secondsToDuration from "../common/datetime/seconds_to_duration";
|
||||
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { formatListWithAnds } from "../common/string/format-list";
|
||||
import { isTemplate } from "../common/string/has-template";
|
||||
@@ -9,7 +10,13 @@ import type { Condition } from "./automation";
|
||||
import { describeCondition } from "./automation_i18n";
|
||||
import { localizeDeviceAutomationAction } from "./device/device_automation";
|
||||
import type { EntityRegistryEntry } from "./entity/entity_registry";
|
||||
import {
|
||||
computeEntityRegistryName,
|
||||
entityRegistryById,
|
||||
} from "./entity/entity_registry";
|
||||
import type { FloorRegistryEntry } from "./floor_registry";
|
||||
import { domainToName } from "./integration";
|
||||
import type { LabelRegistryEntry } from "./label/label_registry";
|
||||
import type {
|
||||
ActionType,
|
||||
ActionTypes,
|
||||
@@ -34,6 +41,8 @@ const actionTranslationBaseKey =
|
||||
export const describeAction = <T extends ActionType>(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
labelRegistry: LabelRegistryEntry[],
|
||||
floorRegistry: Record<string, FloorRegistryEntry>,
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false
|
||||
@@ -42,6 +51,8 @@ export const describeAction = <T extends ActionType>(
|
||||
const description = tryDescribeAction(
|
||||
hass,
|
||||
entityRegistry,
|
||||
labelRegistry,
|
||||
floorRegistry,
|
||||
action,
|
||||
actionType,
|
||||
ignoreAlias
|
||||
@@ -64,6 +75,8 @@ export const describeAction = <T extends ActionType>(
|
||||
const tryDescribeAction = <T extends ActionType>(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
labelRegistry: LabelRegistryEntry[],
|
||||
floorRegistry: Record<string, FloorRegistryEntry>,
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false
|
||||
@@ -87,6 +100,107 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
{ name: "target" }
|
||||
)
|
||||
);
|
||||
} else if (targetOrData) {
|
||||
for (const [key, name] of Object.entries({
|
||||
area_id: "areas",
|
||||
device_id: "devices",
|
||||
entity_id: "entities",
|
||||
floor_id: "floors",
|
||||
label_id: "labels",
|
||||
})) {
|
||||
if (!(key in targetOrData)) {
|
||||
continue;
|
||||
}
|
||||
const keyConf: string[] = ensureArray(targetOrData[key]) || [];
|
||||
|
||||
for (const targetThing of keyConf) {
|
||||
if (isTemplate(targetThing)) {
|
||||
targets.push(
|
||||
hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.target_template`,
|
||||
{ name }
|
||||
)
|
||||
);
|
||||
break;
|
||||
} else if (key === "entity_id") {
|
||||
if (targetThing.includes(".")) {
|
||||
const state = hass.states[targetThing];
|
||||
if (state) {
|
||||
targets.push(computeStateName(state));
|
||||
} else {
|
||||
targets.push(targetThing);
|
||||
}
|
||||
} else {
|
||||
const entityReg = entityRegistryById(entityRegistry)[targetThing];
|
||||
if (entityReg) {
|
||||
targets.push(
|
||||
computeEntityRegistryName(hass, entityReg) || targetThing
|
||||
);
|
||||
} else if (targetThing === "all") {
|
||||
targets.push(
|
||||
hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.target_every_entity`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
targets.push(
|
||||
hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.target_unknown_entity`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (key === "device_id") {
|
||||
const device = hass.devices[targetThing];
|
||||
if (device) {
|
||||
targets.push(computeDeviceNameDisplay(device, hass));
|
||||
} else {
|
||||
targets.push(
|
||||
hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.target_unknown_device`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (key === "area_id") {
|
||||
const area = hass.areas[targetThing];
|
||||
if (area?.name) {
|
||||
targets.push(area.name);
|
||||
} else {
|
||||
targets.push(
|
||||
hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.target_unknown_area`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (key === "floor_id") {
|
||||
const floor = floorRegistry[targetThing] ?? undefined;
|
||||
if (floor?.name) {
|
||||
targets.push(floor.name);
|
||||
} else {
|
||||
targets.push(
|
||||
hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.target_unknown_floor`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (key === "label_id") {
|
||||
const label = labelRegistry.find(
|
||||
(lbl) => lbl.label_id === targetThing
|
||||
);
|
||||
if (label?.name) {
|
||||
targets.push(label.name);
|
||||
} else {
|
||||
targets.push(
|
||||
hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.target_unknown_label`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
targets.push(targetThing);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -115,20 +229,26 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
|
||||
if (config.metadata) {
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.service_name_no_targets`,
|
||||
targets.length
|
||||
? `${actionTranslationBaseKey}.service.description.service_name`
|
||||
: `${actionTranslationBaseKey}.service.description.service_name_no_targets`,
|
||||
{
|
||||
domain: domainToName(hass.localize, domain),
|
||||
name: service || config.action,
|
||||
targets: formatListWithAnds(hass.locale, targets),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.service_based_on_name_no_targets`,
|
||||
targets.length
|
||||
? `${actionTranslationBaseKey}.service.description.service_based_on_name`
|
||||
: `${actionTranslationBaseKey}.service.description.service_based_on_name_no_targets`,
|
||||
{
|
||||
name: service
|
||||
? `${domainToName(hass.localize, domain)}: ${service}`
|
||||
: config.action,
|
||||
targets: formatListWithAnds(hass.locale, targets),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,27 +50,14 @@ class StepFlowForm extends LitElement {
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
}
|
||||
|
||||
private handleReadOnlyFields = memoizeOne((schema) => {
|
||||
function handleReadOnlyField(field: HaFormSchema) {
|
||||
return {
|
||||
private handleReadOnlyFields = memoizeOne((schema) =>
|
||||
schema?.map((field) => ({
|
||||
...field,
|
||||
...(Object.values((field as HaFormSelector)?.selector ?? {})[0]
|
||||
?.read_only
|
||||
...(Object.values((field as HaFormSelector)?.selector ?? {})[0]?.read_only
|
||||
? { disabled: true }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
return schema?.map((field: HaFormSchema) =>
|
||||
field.type === "expandable" && field.schema
|
||||
? {
|
||||
...field,
|
||||
schema: field.schema.map((sectionField) =>
|
||||
handleReadOnlyField(sectionField)
|
||||
),
|
||||
}
|
||||
: handleReadOnlyField(field)
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const step = this.step;
|
||||
|
||||
@@ -37,7 +37,8 @@ class PanelClimate extends LitElement {
|
||||
super.willUpdate(changedProps);
|
||||
// Initial setup
|
||||
if (!this.hasUpdated) {
|
||||
this._setup();
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setLovelace();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,11 +75,6 @@ class PanelClimate extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _setup() {
|
||||
await this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
private _debounceRegistriesChanged = debounce(
|
||||
() => this._registriesChanged(),
|
||||
200
|
||||
|
||||
@@ -103,10 +103,6 @@ const processAreasForClimate = (
|
||||
heading_style: "subtitle",
|
||||
type: "heading",
|
||||
heading: area.name,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: `/home/areas-${area.area_id}`,
|
||||
},
|
||||
});
|
||||
cards.push(...areaCards);
|
||||
}
|
||||
|
||||
@@ -16,10 +16,8 @@ import { slugify } from "../../../common/string/slugify";
|
||||
import { groupBy } from "../../../common/util/group-by";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list";
|
||||
@@ -228,23 +226,32 @@ class HaConfigAreaPage extends LitElement {
|
||||
></ha-icon>`
|
||||
: nothing}${area.name}`}
|
||||
>
|
||||
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown-item value="settings" .entry=${area}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPencil}></ha-svg-icon>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.areas.edit_settings")}
|
||||
</ha-dropdown-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPencil}> </ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-dropdown-item value="delete" variant="danger">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
<ha-list-item
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
@click=${this._deleteConfirm}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.areas.editor.delete")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<ha-svg-icon class="warning" slot="graphic" .path=${mdiDelete}>
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
|
||||
<div class="container">
|
||||
<div class="column">
|
||||
@@ -606,20 +613,6 @@ class HaConfigAreaPage extends LitElement {
|
||||
this._related = await findRelated(this.hass, "area", this.areaId);
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem & {
|
||||
entry: AreaRegistryEntry;
|
||||
};
|
||||
switch (item.value) {
|
||||
case "settings":
|
||||
this._openDialog(item.entry);
|
||||
break;
|
||||
case "delete":
|
||||
this._deleteConfirm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _showSettings(ev: MouseEvent) {
|
||||
const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry;
|
||||
this._openDialog(entry);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
@@ -24,12 +24,10 @@ import {
|
||||
type AreasFloorHierarchy,
|
||||
} from "../../../common/areas/areas-floor-hierarchy";
|
||||
import { formatListWithAnds } from "../../../common/string/format-list";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-floor-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-sortable";
|
||||
import type { HaSortableOptions } from "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
@@ -198,43 +196,44 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
${floor.name}
|
||||
</h2>
|
||||
<div class="actions">
|
||||
<ha-dropdown
|
||||
<ha-button-menu
|
||||
.floor=${floor}
|
||||
@wa-select=${this._handleFloorAction}
|
||||
@action=${this._handleFloorAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="reorder">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
<ha-list-item graphic="icon"
|
||||
><ha-svg-icon
|
||||
.path=${mdiSort}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.reorder"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item value="edit">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
)}</ha-list-item
|
||||
>
|
||||
<li divider role="separator"></li>
|
||||
<ha-list-item graphic="icon"
|
||||
><ha-svg-icon
|
||||
.path=${mdiPencil}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.edit_floor"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="delete" variant="danger">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
)}</ha-list-item
|
||||
>
|
||||
<ha-list-item class="warning" graphic="icon"
|
||||
><ha-svg-icon
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.delete_floor"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
)}</ha-list-item
|
||||
>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</div>
|
||||
<ha-sortable
|
||||
@@ -274,23 +273,23 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
)}
|
||||
</h2>
|
||||
<div class="actions">
|
||||
<ha-dropdown
|
||||
@wa-select=${this._handleUnassignedAreasAction}
|
||||
<ha-button-menu
|
||||
@action=${this._handleUnassignedAreasAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="reorder">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
<ha-list-item graphic="icon"
|
||||
><ha-svg-icon
|
||||
.path=${mdiSort}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.reorder"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
)}</ha-list-item
|
||||
>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</div>
|
||||
<ha-sortable
|
||||
@@ -534,25 +533,23 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
}, time);
|
||||
}
|
||||
|
||||
private _handleFloorAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
private _handleFloorAction(ev: CustomEvent<ActionDetail>) {
|
||||
const floor = (ev.currentTarget as any).floor;
|
||||
switch (item.value) {
|
||||
case "reorder":
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._showReorderDialog();
|
||||
break;
|
||||
case "edit":
|
||||
case 1:
|
||||
this._editFloor(floor);
|
||||
break;
|
||||
case "delete":
|
||||
case 2:
|
||||
this._deleteFloor(floor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleUnassignedAreasAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
if (item.value === "reorder") {
|
||||
private _handleUnassignedAreasAction(ev: CustomEvent<ActionDetail>) {
|
||||
if (ev.detail.index === 0) {
|
||||
this._showReorderDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { dump } from "js-yaml";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
@@ -54,13 +53,18 @@ import type {
|
||||
} from "../../../../data/automation";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import {
|
||||
floorsContext,
|
||||
fullEntitiesContext,
|
||||
labelsContext,
|
||||
} from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../data/floor_registry";
|
||||
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
|
||||
import type {
|
||||
Action,
|
||||
NonConditionAction,
|
||||
RepeatAction,
|
||||
ServiceAction,
|
||||
} from "../../../../data/script";
|
||||
import { getActionType, isAction } from "../../../../data/script";
|
||||
import { describeAction } from "../../../../data/script_i18n";
|
||||
@@ -74,7 +78,6 @@ import { isMac } from "../../../../util/is_mac";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import "../target/ha-automation-row-targets";
|
||||
import "./ha-automation-action-editor";
|
||||
import type HaAutomationActionEditor from "./ha-automation-action-editor";
|
||||
import "./types/ha-automation-action-choose";
|
||||
@@ -173,6 +176,14 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelReg!: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
_floorReg!: Record<string, FloorRegistryEntry>;
|
||||
|
||||
@state() private _uiModeAvailable = true;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
@@ -252,11 +263,14 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
`}
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action)
|
||||
describeAction(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
this.action
|
||||
)
|
||||
)}
|
||||
${type === "service" && "target" in this.action
|
||||
? this._renderTargets((this.action as ServiceAction).target)
|
||||
: nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
@@ -542,14 +556,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTargets = memoizeOne(
|
||||
(target?: HassServiceTarget) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
private _onValueChange(event: CustomEvent) {
|
||||
// reload sidebar if sort, deleted,... happend
|
||||
if (this._selected && this.optionsInSidebar) {
|
||||
@@ -662,7 +668,15 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
),
|
||||
inputType: "string",
|
||||
placeholder: capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action, undefined, true)
|
||||
describeAction(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
this.action,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
),
|
||||
defaultValue: this.action.alias,
|
||||
confirmText: this.hass.localize("ui.common.submit"),
|
||||
|
||||
@@ -18,6 +18,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { mainWindow } from "../../../common/dom/get_main_window";
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeEntityNameList } from "../../../common/entity/compute_entity_name_display";
|
||||
import { computeFloorName } from "../../../common/entity/compute_floor_name";
|
||||
@@ -122,7 +123,6 @@ import "./add-automation-element/ha-automation-add-items";
|
||||
import "./add-automation-element/ha-automation-add-search";
|
||||
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
||||
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
|
||||
import { getTargetText } from "./target/get_target_text";
|
||||
|
||||
const TYPES = {
|
||||
trigger: { collections: TRIGGER_COLLECTIONS, icons: TRIGGER_ICONS },
|
||||
@@ -1393,8 +1393,8 @@ class DialogAddAutomationElement
|
||||
}
|
||||
);
|
||||
|
||||
private _getLabel = memoizeOne((id: string) =>
|
||||
this._labelRegistry?.find(({ label_id }) => label_id === id)
|
||||
private _getLabel = memoizeOne((labelId) =>
|
||||
this._labelRegistry?.find(({ label_id }) => label_id === labelId)
|
||||
);
|
||||
|
||||
private _getDomainType(domain: string) {
|
||||
@@ -1926,12 +1926,32 @@ class DialogAddAutomationElement
|
||||
}
|
||||
|
||||
if (targetId) {
|
||||
return getTargetText(
|
||||
this.hass,
|
||||
targetType as "floor" | "area" | "device" | "entity" | "label",
|
||||
targetId,
|
||||
this._getLabel
|
||||
if (targetType === "floor") {
|
||||
return computeFloorName(this.hass.floors[targetId]) || targetId;
|
||||
}
|
||||
if (targetType === "area") {
|
||||
return computeAreaName(this.hass.areas[targetId]) || targetId;
|
||||
}
|
||||
if (targetType === "device") {
|
||||
return computeDeviceName(this.hass.devices[targetId]) || targetId;
|
||||
}
|
||||
if (targetType === "entity" && this.hass.states[targetId]) {
|
||||
const stateObj = this.hass.states[targetId];
|
||||
const [entityName, deviceName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
return entityName || deviceName || targetId;
|
||||
}
|
||||
if (targetType === "label") {
|
||||
const label = this._getLabel(targetId);
|
||||
return label?.name || targetId;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -20,13 +20,13 @@ import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-floor-icon";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-section-title";
|
||||
import "../../../../components/ha-state-icon";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import {
|
||||
getAreasNestedInFloors,
|
||||
@@ -779,11 +779,11 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
|
||||
private _renderEntityIcon =
|
||||
(stateObj: HassEntity) => (slot: string | undefined) =>
|
||||
html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
html`<state-badge
|
||||
slot=${ifDefined(slot)}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>`;
|
||||
.hass=${this.hass}
|
||||
></state-badge>`;
|
||||
|
||||
private _renderItem(
|
||||
label: string,
|
||||
@@ -1435,7 +1435,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
|
||||
ha-svg-icon,
|
||||
ha-icon,
|
||||
ha-state-icon,
|
||||
ha-floor-icon {
|
||||
padding: var(--ha-space-1);
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
@@ -1460,6 +1459,13 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
wa-tree-item[selected],
|
||||
wa-tree-item[selected] > ha-svg-icon,
|
||||
wa-tree-item[selected] > ha-icon,
|
||||
wa-tree-item[selected] > ha-floor-icon {
|
||||
color: var(--ha-color-on-primary-normal);
|
||||
}
|
||||
|
||||
wa-tree-item[selected]::part(item):hover {
|
||||
background-color: var(--ha-color-fill-primary-normal-hover);
|
||||
}
|
||||
@@ -1484,11 +1490,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
--icon-primary-color: var(--ha-color-on-primary-normal);
|
||||
}
|
||||
|
||||
wa-tree-item[selected],
|
||||
wa-tree-item[selected] > ha-svg-icon,
|
||||
wa-tree-item[selected] > ha-icon,
|
||||
wa-tree-item[selected] > ha-state-icon,
|
||||
wa-tree-item[selected] > ha-floor-icon,
|
||||
ha-md-list-item.selected ha-icon,
|
||||
ha-md-list-item.selected ha-svg-icon {
|
||||
color: var(--ha-color-on-primary-normal);
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { mdiInformationOutline, mdiPlus } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import {
|
||||
mdiInformationOutline,
|
||||
mdiLabel,
|
||||
mdiPlus,
|
||||
mdiTextureBox,
|
||||
} from "@mdi/js";
|
||||
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -12,15 +17,17 @@ import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-domain-icon";
|
||||
import "../../../../components/ha-floor-icon";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import type { ConfigEntry } from "../../../../data/config_entries";
|
||||
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { AddAutomationElementListItem } from "../add-automation-element-dialog";
|
||||
import { getTargetIcon } from "../target/get_target_icon";
|
||||
|
||||
type Target = [string, string | undefined, string | undefined];
|
||||
|
||||
@@ -43,7 +50,7 @@ export class HaAutomationAddItems extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public getLabel!: (
|
||||
id: string
|
||||
) => LabelRegistryEntry | undefined;
|
||||
) => { name: string; icon?: string } | undefined;
|
||||
|
||||
@property({ attribute: false }) public configEntryLookup: Record<
|
||||
string,
|
||||
@@ -157,17 +164,72 @@ export class HaAutomationAddItems extends LitElement {
|
||||
}
|
||||
|
||||
return html`<div class="selected-target">
|
||||
${getTargetIcon(
|
||||
this.hass,
|
||||
target[0],
|
||||
target[1],
|
||||
this.configEntryLookup,
|
||||
this.getLabel
|
||||
)}
|
||||
${this._getSelectedTargetIcon(target[0], target[1])}
|
||||
<div class="label">${target[2]}</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
private _getSelectedTargetIcon(
|
||||
targetType: string,
|
||||
targetId: string | undefined
|
||||
): TemplateResult | typeof nothing {
|
||||
if (!targetId) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (targetType === "floor") {
|
||||
return html`<ha-floor-icon
|
||||
.floor=${this.hass.floors[targetId]}
|
||||
></ha-floor-icon>`;
|
||||
}
|
||||
|
||||
if (targetType === "area" && this.hass.areas[targetId]) {
|
||||
const area = this.hass.areas[targetId];
|
||||
if (area.icon) {
|
||||
return html`<ha-icon .icon=${area.icon}></ha-icon>`;
|
||||
}
|
||||
return html`<ha-svg-icon .path=${mdiTextureBox}></ha-svg-icon>`;
|
||||
}
|
||||
|
||||
if (targetType === "device" && this.hass.devices[targetId]) {
|
||||
const device = this.hass.devices[targetId];
|
||||
const configEntry = device.primary_config_entry
|
||||
? this.configEntryLookup[device.primary_config_entry]
|
||||
: undefined;
|
||||
const domain = configEntry?.domain;
|
||||
|
||||
if (domain) {
|
||||
return html`<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetType === "entity" && this.hass.states[targetId]) {
|
||||
const stateObj = this.hass.states[targetId];
|
||||
if (stateObj) {
|
||||
return html`<state-badge
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
.stateColor=${false}
|
||||
></state-badge>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetType === "label") {
|
||||
const label = this.getLabel(targetId);
|
||||
if (label?.icon) {
|
||||
return html`<ha-icon .icon=${label.icon}></ha-icon>`;
|
||||
}
|
||||
return html`<ha-svg-icon .path=${mdiLabel}></ha-svg-icon>`;
|
||||
}
|
||||
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _selected(ev) {
|
||||
const item = ev.currentTarget;
|
||||
fireEvent(this, "value-changed", {
|
||||
@@ -273,6 +335,10 @@ export class HaAutomationAddItems extends LitElement {
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
}
|
||||
|
||||
ha-icon-next {
|
||||
width: var(--ha-space-6);
|
||||
}
|
||||
|
||||
ha-svg-icon.plus {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
@@ -296,11 +362,16 @@ export class HaAutomationAddItems extends LitElement {
|
||||
|
||||
.selected-target ha-icon,
|
||||
.selected-target ha-svg-icon,
|
||||
.selected-target state-badge,
|
||||
.selected-target ha-domain-icon {
|
||||
display: flex;
|
||||
padding: var(--ha-space-1) 0;
|
||||
}
|
||||
|
||||
.selected-target state-badge {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.selected-target state-badge,
|
||||
.selected-target ha-floor-icon {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -44,7 +43,6 @@ import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
ConditionSidebarConfig,
|
||||
PlatformCondition,
|
||||
} from "../../../../data/automation";
|
||||
import { isCondition, testCondition } from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
@@ -62,7 +60,6 @@ import { isMac } from "../../../../util/is_mac";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import "../target/ha-automation-row-targets";
|
||||
import "./ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "./ha-automation-condition-editor";
|
||||
import "./types/ha-automation-condition-and";
|
||||
@@ -194,10 +191,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
)}
|
||||
${"target" in
|
||||
(this.conditionDescriptions[this.condition.condition] || {})
|
||||
? this._renderTargets((this.condition as PlatformCondition).target)
|
||||
: nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
@@ -482,14 +475,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTargets = memoizeOne(
|
||||
(target?: HassServiceTarget) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronRight,
|
||||
mdiCog,
|
||||
mdiContentDuplicate,
|
||||
mdiDelete,
|
||||
@@ -23,7 +23,7 @@ import { differenceInDays } from "date-fns";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
@@ -50,9 +50,6 @@ import type {
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-blueprints";
|
||||
import "../../../components/ha-filter-categories";
|
||||
@@ -61,7 +58,12 @@ import "../../../components/ha-filter-entities";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-icon-button";
|
||||
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu";
|
||||
import type { HaMdMenu } from "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
@@ -173,6 +175,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
|
||||
@state() private _overflowAutomation?: AutomationItem;
|
||||
|
||||
@storage({ key: "automation-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@@ -200,6 +204,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu!: HaMdMenu;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
@@ -362,81 +368,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
template: (automation) => html`
|
||||
<ha-dropdown
|
||||
.automation=${automation}
|
||||
@wa-select=${this._handleRowOverflowMenu}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.automation=${automation}
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
@click=${this._showOverflowMenu}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="show-info">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.show_info"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="show-settings">
|
||||
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_settings"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="edit-category">
|
||||
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.picker.${automation.category ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="run-actions">
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.run")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="show-trace">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiTransitConnection}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.show_trace"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item value="duplicate">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.duplicate"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="toggle">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${automation.state === "off"
|
||||
? mdiToggleSwitch
|
||||
: mdiToggleSwitchOffOutline}
|
||||
></ha-svg-icon>
|
||||
${automation.state === "off"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.disable"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="delete" variant="danger">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.delete"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`,
|
||||
},
|
||||
};
|
||||
@@ -444,38 +381,17 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
);
|
||||
|
||||
private _handleRowOverflowMenu = (ev: CustomEvent) => {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
const automation = (
|
||||
(ev.currentTarget as HTMLElement).closest("ha-dropdown") as any
|
||||
).automation;
|
||||
|
||||
switch (item.value) {
|
||||
case "show-info":
|
||||
this._showInfo(automation);
|
||||
break;
|
||||
case "show-settings":
|
||||
this._showSettings(automation);
|
||||
break;
|
||||
case "edit-category":
|
||||
this._editCategory(automation);
|
||||
break;
|
||||
case "run-actions":
|
||||
this._runActions(automation);
|
||||
break;
|
||||
case "show-trace":
|
||||
this._showTrace(automation);
|
||||
break;
|
||||
case "duplicate":
|
||||
this._duplicate(automation);
|
||||
break;
|
||||
case "toggle":
|
||||
this._toggle(automation);
|
||||
break;
|
||||
case "delete":
|
||||
this._deleteConfirm(automation);
|
||||
break;
|
||||
private _showOverflowMenu = (ev) => {
|
||||
if (
|
||||
this._overflowMenu.open &&
|
||||
ev.target === this._overflowMenu.anchorElement
|
||||
) {
|
||||
this._overflowMenu.close();
|
||||
return;
|
||||
}
|
||||
this._overflowAutomation = ev.target.automation;
|
||||
this._overflowMenu.anchorElement = ev.target;
|
||||
this._overflowMenu.show();
|
||||
};
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
@@ -493,38 +409,34 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
private _renderCategoryItems = (submenu = false) =>
|
||||
html`${this._categories?.map(
|
||||
protected render(): TemplateResult {
|
||||
const categoryItems = html`${this._categories?.map(
|
||||
(category) =>
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_category"
|
||||
data-category=${category.category_id}
|
||||
html`<ha-md-menu-item
|
||||
.value=${category.category_id}
|
||||
.clickAction=${this._handleBulkCategory}
|
||||
>
|
||||
${category.icon
|
||||
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`}
|
||||
${category.name}
|
||||
</ha-dropdown-item>`
|
||||
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
|
||||
<div slot="headline">${category.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="__no_category__"
|
||||
>
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="__create_category__"
|
||||
>
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</ha-dropdown-item>`;
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
|
||||
private _renderLabelItems = (submenu = false) =>
|
||||
html`${this._labels?.map((label) => {
|
||||
const labelItems = html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
@@ -534,14 +446,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this._selected.some((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
return html`<ha-md-menu-item
|
||||
.value=${label.label_id}
|
||||
data-action=${selected ? "remove" : "add"}
|
||||
.action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
keep-open
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="icon"
|
||||
slot="start"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
@@ -555,50 +467,46 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-dropdown-item>`;
|
||||
</ha-md-menu-item>`;
|
||||
})}
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="__create_label__"
|
||||
@click=${this._bulkCreateLabel}
|
||||
>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</ha-dropdown-item>`;
|
||||
</div></ha-md-menu-item
|
||||
>`;
|
||||
|
||||
private _renderAreaItems = (submenu = false) =>
|
||||
html`${Object.values(this.hass.areas).map(
|
||||
const areaItems = html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_area"
|
||||
data-area=${area.area_id}
|
||||
html`<ha-md-menu-item
|
||||
.value=${area.area_id}
|
||||
.clickAction=${this._handleBulkArea}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="icon"
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
${area.name}
|
||||
</ha-dropdown-item>`
|
||||
<div slot="headline">${area.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="__no_area__">
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="__create_area__"
|
||||
>
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</ha-dropdown-item>`;
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const areasInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 900) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
@@ -619,9 +527,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config"}
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
id="entity_id"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
@@ -633,14 +541,16 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
has-filters
|
||||
.filters=${Object.values(this._filters).filter((filter) =>
|
||||
.filters=${
|
||||
Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter.value)
|
||||
? filter.value.length
|
||||
: filter.value &&
|
||||
Object.values(filter.value).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
).length
|
||||
}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.localize,
|
||||
@@ -733,11 +643,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-blueprints>
|
||||
${!this.narrow
|
||||
? html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
${
|
||||
!this.narrow
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -749,11 +657,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderCategoryItems()}
|
||||
</ha-dropdown>
|
||||
${categoryItems}
|
||||
</ha-md-button-menu>
|
||||
${labelsInOverflow
|
||||
? nothing
|
||||
: html`<ha-dropdown slot="selection-bar">
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -765,14 +673,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderLabelItems()}
|
||||
</ha-dropdown>`}
|
||||
${labelItems}
|
||||
</ha-md-button-menu>`}
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -784,15 +689,13 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderAreaItems()}
|
||||
</ha-dropdown>`}`
|
||||
: nothing}
|
||||
<ha-dropdown
|
||||
has-overflow
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
${this.narrow
|
||||
${areaItems}
|
||||
</ha-md-button-menu>`}`
|
||||
: nothing
|
||||
}
|
||||
<ha-md-button-menu has-overflow slot="selection-bar">
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
@@ -810,48 +713,89 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html`<ha-dropdown-item>
|
||||
></ha-icon-button>`
|
||||
}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
)}
|
||||
${this._renderCategoryItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || labelsInOverflow
|
||||
? html`<ha-dropdown-item>
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || labelsInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
${this._renderLabelItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || areasInOverflow
|
||||
? html`<ha-dropdown-item>
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || areasInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
${this._renderAreaItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
<ha-dropdown-item value="enable">
|
||||
<ha-svg-icon slot="icon" .path=${mdiToggleSwitch}></ha-svg-icon>
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${areaItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
<ha-md-menu-item .clickAction=${this._handleBulkEnable}>
|
||||
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.enable"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="disable">
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._handleBulkDisable}>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
slot="start"
|
||||
.path=${mdiToggleSwitchOffOutline}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.disable"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
${!this.automations.length
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
${
|
||||
!this.automations.length
|
||||
? html`<div class="empty" slot="empty">
|
||||
<ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon>
|
||||
<h1>
|
||||
@@ -871,7 +815,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
)}
|
||||
</p>
|
||||
<ha-button
|
||||
href=${documentationUrl(this.hass, "/docs/automation/editor/")}
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/automation/editor/"
|
||||
)}
|
||||
target="_blank"
|
||||
appearance="plain"
|
||||
rel="noreferrer"
|
||||
@@ -881,7 +828,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}> </ha-svg-icon>
|
||||
</ha-button>
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
.label=${this.hass.localize(
|
||||
@@ -893,6 +841,80 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-md-menu id="overflow-menu" positioning="fixed">
|
||||
<ha-md-menu-item .clickAction=${this._showInfo}>
|
||||
<ha-svg-icon
|
||||
.path=${mdiInformationOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._showSettings}>
|
||||
<ha-svg-icon .path=${mdiCog} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_settings"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._editCategory}>
|
||||
<ha-svg-icon .path=${mdiTag} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._runActions}>
|
||||
<ha-svg-icon .path=${mdiPlay} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.editor.run")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._showTrace}>
|
||||
<ha-svg-icon .path=${mdiTransitConnection} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.show_trace"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._duplicate}>
|
||||
<ha-svg-icon .path=${mdiContentDuplicate} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._toggle}>
|
||||
<ha-svg-icon
|
||||
.path=${
|
||||
this._overflowAutomation?.state === "off"
|
||||
? mdiToggleSwitch
|
||||
: mdiToggleSwitchOffOutline
|
||||
}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${
|
||||
this._overflowAutomation?.state === "off"
|
||||
? this.hass.localize("ui.panel.config.automation.editor.enable")
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.disable"
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._deleteConfirm} class="warning">
|
||||
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.picker.delete")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1049,22 +1071,33 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _showInfo = (automation: AutomationItem) => {
|
||||
private _showInfo = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
fireEvent(this, "hass-more-info", { entityId: automation.entity_id });
|
||||
};
|
||||
|
||||
private _showSettings = (automation: AutomationItem) => {
|
||||
private _showSettings = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: automation.entity_id,
|
||||
view: "settings",
|
||||
});
|
||||
};
|
||||
|
||||
private _runActions = (automation: AutomationItem) => {
|
||||
private _runActions = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
triggerAutomationActions(this.hass, automation.entity_id);
|
||||
};
|
||||
|
||||
private _editCategory = (automation: AutomationItem) => {
|
||||
private _editCategory = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
const entityReg = this._entityReg.find(
|
||||
(reg) => reg.entity_id === automation.entity_id
|
||||
);
|
||||
@@ -1085,7 +1118,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private _showTrace = (automation: AutomationItem) => {
|
||||
private _showTrace = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
if (!automation.attributes.id) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
@@ -1099,14 +1135,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
};
|
||||
|
||||
private _toggle = async (automation: AutomationItem): Promise<void> => {
|
||||
private _toggle = async (item: HaMdMenuItem): Promise<void> => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
const service = automation.state === "off" ? "turn_on" : "turn_off";
|
||||
await this.hass.callService("automation", service, {
|
||||
entity_id: automation.entity_id,
|
||||
});
|
||||
};
|
||||
|
||||
private _deleteConfirm = async (automation: AutomationItem) => {
|
||||
private _deleteConfirm = async (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.picker.delete_confirm_title"
|
||||
@@ -1122,11 +1164,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private async _delete(automation: AutomationEntity) {
|
||||
private async _delete(automation) {
|
||||
try {
|
||||
if (!automation.attributes.id) {
|
||||
throw new Error("Automation ID is missing");
|
||||
}
|
||||
await deleteAutomation(this.hass, automation.attributes.id);
|
||||
this._selected = this._selected.filter(
|
||||
(entityId) => entityId !== automation.entity_id
|
||||
@@ -1146,11 +1185,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _duplicate = async (automation: AutomationEntity) => {
|
||||
private _duplicate = async (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
try {
|
||||
if (!automation.attributes.id) {
|
||||
throw new Error("Automation ID is missing");
|
||||
}
|
||||
const config = await fetchAutomationFileConfig(
|
||||
this.hass,
|
||||
automation.attributes.id
|
||||
@@ -1222,6 +1261,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleBulkCategory = async (item) => {
|
||||
const category = item.value;
|
||||
this._bulkAddCategory(category);
|
||||
};
|
||||
|
||||
private async _bulkAddCategory(category: string) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1248,9 +1292,8 @@ ${rejected
|
||||
}
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
ev.stopPropagation();
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.dataset.action;
|
||||
const action = ev.currentTarget.action;
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
@@ -1284,6 +1327,11 @@ ${rejected
|
||||
}
|
||||
}
|
||||
|
||||
private _handleBulkArea = (item) => {
|
||||
const area = item.value;
|
||||
this._bulkAddArea(area);
|
||||
};
|
||||
|
||||
private async _bulkAddArea(area: string) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1319,40 +1367,6 @@ ${rejected
|
||||
});
|
||||
};
|
||||
|
||||
private _handleOverflowMenuSelect = (ev: CustomEvent) => {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "enable":
|
||||
this._handleBulkEnable();
|
||||
break;
|
||||
case "disable":
|
||||
this._handleBulkDisable();
|
||||
break;
|
||||
case "__create_category__":
|
||||
this._bulkCreateCategory();
|
||||
break;
|
||||
case "__no_category__":
|
||||
this._bulkAddCategory("");
|
||||
break;
|
||||
case "move_category":
|
||||
if (item.dataset.category) {
|
||||
this._bulkAddCategory(item.dataset.category);
|
||||
}
|
||||
break;
|
||||
case "__create_area__":
|
||||
this._bulkCreateArea();
|
||||
break;
|
||||
case "__no_area__":
|
||||
this._bulkAddArea("");
|
||||
break;
|
||||
case "move_area":
|
||||
if (item.dataset.area) {
|
||||
this._bulkAddArea(item.dataset.area);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private _handleBulkEnable = async () => {
|
||||
const promises: Promise<ServiceCallResponse>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1463,7 +1477,7 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-dropdown ha-assist-chip {
|
||||
ha-md-button-menu ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
ha-label {
|
||||
|
||||
@@ -14,12 +14,6 @@ export const rowStyles = css`
|
||||
h3 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-2) 0;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { mdiLabel, mdiTextureBox } from "@mdi/js";
|
||||
import { html, nothing, type TemplateResult } from "lit";
|
||||
import "../../../../components/ha-domain-icon";
|
||||
import "../../../../components/ha-floor-icon";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-state-icon";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { ConfigEntry } from "../../../../data/config_entries";
|
||||
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
export const getTargetIcon = (
|
||||
hass: HomeAssistant,
|
||||
targetType: string,
|
||||
targetId: string | undefined,
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
getLabel?: (id: string) => LabelRegistryEntry | undefined
|
||||
): TemplateResult | typeof nothing => {
|
||||
if (!targetId) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (targetType === "floor" && hass.floors[targetId]) {
|
||||
return html`<ha-floor-icon
|
||||
.floor=${hass.floors[targetId]}
|
||||
></ha-floor-icon>`;
|
||||
}
|
||||
|
||||
if (targetType === "area") {
|
||||
const area = hass.areas[targetId];
|
||||
if (area?.icon) {
|
||||
return html`<ha-icon .icon=${area.icon}></ha-icon>`;
|
||||
}
|
||||
return html`<ha-svg-icon .path=${mdiTextureBox}></ha-svg-icon>`;
|
||||
}
|
||||
|
||||
if (targetType === "device" && hass.devices[targetId]) {
|
||||
const device = hass.devices[targetId];
|
||||
const configEntry = device.primary_config_entry
|
||||
? configEntryLookup[device.primary_config_entry]
|
||||
: undefined;
|
||||
const domain = configEntry?.domain;
|
||||
|
||||
if (domain) {
|
||||
return html`<ha-domain-icon
|
||||
.hass=${hass}
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetType === "entity" && hass.states[targetId]) {
|
||||
return html`<ha-state-icon
|
||||
.hass=${hass}
|
||||
.stateObj=${hass.states[targetId]}
|
||||
></ha-state-icon>`;
|
||||
}
|
||||
|
||||
if (targetType === "label" && getLabel) {
|
||||
const label = getLabel(targetId);
|
||||
if (label?.icon) {
|
||||
return html`<ha-icon .icon=${label.icon}></ha-icon>`;
|
||||
}
|
||||
return html`<ha-svg-icon .path=${mdiLabel}></ha-svg-icon>`;
|
||||
}
|
||||
|
||||
return nothing;
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { computeFloorName } from "../../../../common/entity/compute_floor_name";
|
||||
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
export const getTargetText = (
|
||||
hass: HomeAssistant,
|
||||
targetType: "floor" | "area" | "device" | "entity" | "label",
|
||||
targetId: string,
|
||||
getLabel?: (id: string) => LabelRegistryEntry | undefined
|
||||
): string => {
|
||||
if (targetType === "floor") {
|
||||
return (
|
||||
(hass.floors[targetId] && computeFloorName(hass.floors[targetId])) ||
|
||||
hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.service.description.target_unknown_floor"
|
||||
)
|
||||
);
|
||||
}
|
||||
if (targetType === "area") {
|
||||
return (
|
||||
(hass.areas[targetId] && computeAreaName(hass.areas[targetId])) ||
|
||||
hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.service.description.target_unknown_area"
|
||||
)
|
||||
);
|
||||
}
|
||||
if (targetType === "device") {
|
||||
return (
|
||||
(hass.devices[targetId] && computeDeviceName(hass.devices[targetId])) ||
|
||||
hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.service.description.target_unknown_device"
|
||||
)
|
||||
);
|
||||
}
|
||||
if (targetType === "entity" && hass.states[targetId]) {
|
||||
const stateObj = hass.states[targetId];
|
||||
const [entityName, deviceName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
return entityName || deviceName || targetId;
|
||||
}
|
||||
if (targetType === "entity") {
|
||||
return hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.service.description.target_unknown_entity"
|
||||
);
|
||||
}
|
||||
|
||||
if (targetType === "label" && getLabel) {
|
||||
const label = getLabel(targetId);
|
||||
return (
|
||||
label?.name ||
|
||||
hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.service.description.target_unknown_label"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return targetId;
|
||||
};
|
||||
@@ -1,260 +0,0 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiAlert, mdiFormatListBulleted, mdiShape } from "@mdi/js";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import {
|
||||
getConfigEntries,
|
||||
type ConfigEntry,
|
||||
} from "../../../../data/config_entries";
|
||||
import {
|
||||
areasContext,
|
||||
devicesContext,
|
||||
floorsContext,
|
||||
labelsContext,
|
||||
localizeContext,
|
||||
statesContext,
|
||||
} from "../../../../data/context";
|
||||
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { getTargetIcon } from "./get_target_icon";
|
||||
import { getTargetText } from "./get_target_text";
|
||||
|
||||
@customElement("ha-automation-row-targets")
|
||||
export class HaAutomationRowTargets extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public target?: HassServiceTarget;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: HomeAssistant["localize"];
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
private floors!: HomeAssistant["floors"];
|
||||
|
||||
@state()
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
private areas!: HomeAssistant["areas"];
|
||||
|
||||
@state()
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
private devices!: HomeAssistant["devices"];
|
||||
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private states!: HomeAssistant["states"];
|
||||
|
||||
@state()
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
private _labelRegistry!: LabelRegistryEntry[];
|
||||
|
||||
private _configEntryLookup?: Record<string, ConfigEntry>;
|
||||
|
||||
protected render() {
|
||||
const length = Object.keys(this.target || {}).length;
|
||||
if (!length) {
|
||||
return html`<span class="target">
|
||||
<div class="label">
|
||||
${this.localize(
|
||||
"ui.panel.config.automation.editor.target_summary.no_target"
|
||||
)}
|
||||
</div>
|
||||
</span>`;
|
||||
}
|
||||
const totalLength = Object.values(this.target || {}).reduce(
|
||||
(acc, val) => acc + ensureArray(val).length,
|
||||
0
|
||||
);
|
||||
|
||||
if (totalLength <= 5) {
|
||||
const targets = Object.entries(this.target!).reduce<
|
||||
["floor" | "area" | "device" | "entity" | "label", string][]
|
||||
>((acc, [targetType, targetId]) => {
|
||||
const type = targetType.replace("_id", "") as
|
||||
| "floor"
|
||||
| "area"
|
||||
| "device"
|
||||
| "entity"
|
||||
| "label";
|
||||
return [
|
||||
...acc,
|
||||
...ensureArray(targetId).map((id): [typeof type, string] => [
|
||||
type,
|
||||
id,
|
||||
]),
|
||||
];
|
||||
}, []);
|
||||
|
||||
return targets.map(
|
||||
([targetType, targetId]) =>
|
||||
html`<span class="target-wrapper">
|
||||
${this._renderTarget(targetType, targetId)}
|
||||
</span>`
|
||||
);
|
||||
}
|
||||
|
||||
return html`<span class="target">
|
||||
<ha-svg-icon .path=${mdiFormatListBulleted}></ha-svg-icon>
|
||||
<div class="label">
|
||||
${this.localize(
|
||||
"ui.panel.config.automation.editor.target_summary.targets",
|
||||
{
|
||||
count: totalLength,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</span>`;
|
||||
}
|
||||
|
||||
private _getLabel = (id: string) =>
|
||||
this._labelRegistry?.find(({ label_id }) => label_id === id);
|
||||
|
||||
private _checkTargetExists(
|
||||
targetType: "floor" | "area" | "device" | "entity" | "label",
|
||||
targetId: string
|
||||
): boolean {
|
||||
if (targetType === "floor") {
|
||||
return !!this.floors[targetId];
|
||||
}
|
||||
if (targetType === "area") {
|
||||
return !!this.areas[targetId];
|
||||
}
|
||||
if (targetType === "device") {
|
||||
return !!this.devices[targetId];
|
||||
}
|
||||
if (targetType === "entity") {
|
||||
return !!this.states[targetId];
|
||||
}
|
||||
if (targetType === "label") {
|
||||
return !!this._getLabel(targetId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _renderTargetBadge(
|
||||
icon: TemplateResult | typeof nothing,
|
||||
label: string,
|
||||
alert = false
|
||||
) {
|
||||
return html`<div class="target ${alert ? "alert" : ""}">
|
||||
${icon}
|
||||
<div class="label">${label}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
this._configEntryLookup = Object.fromEntries(
|
||||
configEntries.map((entry) => [entry.entry_id, entry])
|
||||
);
|
||||
}
|
||||
|
||||
private _renderTarget(
|
||||
targetType: "floor" | "area" | "device" | "entity" | "label",
|
||||
targetId: string
|
||||
) {
|
||||
if (targetType === "entity" && ["all", "none"].includes(targetId)) {
|
||||
return this._renderTargetBadge(
|
||||
html`<ha-svg-icon .path=${mdiShape}></ha-svg-icon>`,
|
||||
this.localize(
|
||||
`ui.panel.config.automation.editor.target_summary.${targetId as "all" | "none"}_entities`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const exists = this._checkTargetExists(targetType, targetId);
|
||||
if (!exists) {
|
||||
return this._renderTargetBadge(
|
||||
html`<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>`,
|
||||
getTargetText(this.hass, targetType, targetId, this._getLabel),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if (targetType === "device" && !this._configEntryLookup) {
|
||||
const loadConfigEntries = this._loadConfigEntries().then(() =>
|
||||
this._renderTargetBadge(
|
||||
getTargetIcon(
|
||||
this.hass,
|
||||
targetType,
|
||||
targetId,
|
||||
this._configEntryLookup!
|
||||
),
|
||||
getTargetText(this.hass, targetType, targetId)
|
||||
)
|
||||
);
|
||||
|
||||
return html`${until(loadConfigEntries, nothing)}`;
|
||||
}
|
||||
|
||||
return this._renderTargetBadge(
|
||||
getTargetIcon(
|
||||
this.hass,
|
||||
targetType,
|
||||
targetId,
|
||||
this._configEntryLookup || {},
|
||||
this._getLabel
|
||||
),
|
||||
getTargetText(this.hass, targetType, targetId, this._getLabel)
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: contents;
|
||||
min-height: 32px;
|
||||
}
|
||||
.target-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
gap: var(--ha-space-1);
|
||||
}
|
||||
.target {
|
||||
display: inline-flex;
|
||||
gap: var(--ha-space-1);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
background: var(--ha-color-fill-neutral-normal-resting);
|
||||
padding: 0 var(--ha-space-2) 0 var(--ha-space-1);
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
overflow: hidden;
|
||||
height: 32px;
|
||||
}
|
||||
.target.alert {
|
||||
background: var(--ha-color-fill-warning-normal-resting);
|
||||
color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
.target .label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.target ha-icon,
|
||||
.target ha-svg-icon,
|
||||
.target ha-domain-icon {
|
||||
display: flex;
|
||||
padding: var(--ha-space-1) 0;
|
||||
}
|
||||
|
||||
.target ha-floor-icon {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-row-targets": HaAutomationRowTargets;
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,7 @@ import {
|
||||
mdiRenameBox,
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import type {
|
||||
HassServiceTarget,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -47,7 +44,6 @@ import "../../../../components/ha-svg-icon";
|
||||
import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
PlatformTrigger,
|
||||
Trigger,
|
||||
TriggerList,
|
||||
TriggerSidebarConfig,
|
||||
@@ -68,7 +64,6 @@ import { isMac } from "../../../../util/is_mac";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import "../target/ha-automation-row-targets";
|
||||
import "./ha-automation-trigger-editor";
|
||||
import type HaAutomationTriggerEditor from "./ha-automation-trigger-editor";
|
||||
import "./types/ha-automation-trigger-calendar";
|
||||
@@ -210,11 +205,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
></ha-trigger-icon>`}
|
||||
<h3 slot="header">
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
${type === "platform" &&
|
||||
"target" in
|
||||
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger]
|
||||
? this._renderTargets((this.trigger as PlatformTrigger).target)
|
||||
: nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
@@ -460,14 +450,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTargets = memoizeOne(
|
||||
(target?: HassServiceTarget) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
// on yaml toggle --> clear warnings
|
||||
if (changedProperties.has("yamlMode")) {
|
||||
|
||||
@@ -6,10 +6,8 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-tip";
|
||||
import type {
|
||||
@@ -55,26 +53,26 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
header="Home Assistant Cloud"
|
||||
>
|
||||
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
|
||||
<ha-button-menu slot="toolbar-icon" @action=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown-item value="reset">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDeleteForever}></ha-svg-icon>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.reset_cloud_data"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="download">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiDeleteForever}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.download_support_package"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<div class="content">
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header">Home Assistant Cloud</span>
|
||||
@@ -299,15 +297,13 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "reset":
|
||||
private _handleMenuAction(ev) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._deleteCloudData();
|
||||
break;
|
||||
case "download":
|
||||
case 1:
|
||||
this._downloadSupportPackage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,8 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-list";
|
||||
import "../../../../components/ha-list-item";
|
||||
@@ -46,26 +44,26 @@ export class CloudLoginPanel extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
header="Home Assistant Cloud"
|
||||
>
|
||||
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
|
||||
<ha-button-menu slot="toolbar-icon" @action=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown-item value="reset">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDeleteForever}></ha-svg-icon>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.reset_cloud_data"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="download">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiDeleteForever}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.download_support_package"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<div class="content">
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header">Home Assistant Cloud</span>
|
||||
@@ -166,15 +164,13 @@ export class CloudLoginPanel extends LitElement {
|
||||
fireEvent(this, "flash-message-changed", { value: "" });
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "reset":
|
||||
private _handleMenuAction(ev) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._deleteCloudData();
|
||||
break;
|
||||
case "download":
|
||||
case 1:
|
||||
this._downloadSupportPackage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
@@ -6,12 +6,13 @@ import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-bar";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-metric";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type {
|
||||
@@ -72,24 +73,24 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._checkUpdates}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown @wa-select=${this._handleMenuAction}>
|
||||
<ha-button-menu multi>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item
|
||||
type="checkbox"
|
||||
.checked=${this._showSkipped}
|
||||
value="toggle_skipped"
|
||||
<ha-check-list-item
|
||||
left
|
||||
@request-selected=${this._toggleSkipped}
|
||||
.selected=${this._showSkipped}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.updates.show_skipped")}
|
||||
</ha-dropdown-item>
|
||||
</ha-check-list-item>
|
||||
${this._supervisorInfo
|
||||
? html`
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item
|
||||
value="toggle_beta"
|
||||
<li divider role="separator"></li>
|
||||
<ha-list-item
|
||||
@request-selected=${this._toggleBeta}
|
||||
.disabled=${this._supervisorInfo.channel === "dev"}
|
||||
>
|
||||
${this._supervisorInfo.channel === "stable"
|
||||
@@ -97,10 +98,10 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.updates.leave_beta"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-list-item>
|
||||
`
|
||||
: ""}
|
||||
</ha-dropdown>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
<div class="content">
|
||||
<ha-card outlined>
|
||||
@@ -132,13 +133,21 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev: CustomEvent): void {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "toggle_skipped":
|
||||
private _toggleSkipped(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (ev.detail.source !== "property") {
|
||||
return;
|
||||
}
|
||||
|
||||
this._showSkipped = !this._showSkipped;
|
||||
break;
|
||||
case "toggle_beta":
|
||||
}
|
||||
|
||||
private async _toggleBeta(
|
||||
ev: CustomEvent<RequestSelectedDetail>
|
||||
): Promise<void> {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._supervisorInfo!.channel === "stable") {
|
||||
showJoinBetaDialog(this, {
|
||||
join: async () => this._setChannel("beta"),
|
||||
@@ -146,8 +155,6 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
} else {
|
||||
this._setChannel("stable");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _setChannel(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiCloudLock,
|
||||
mdiDotsVertical,
|
||||
@@ -12,12 +13,11 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-menu-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tip";
|
||||
@@ -226,25 +226,25 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMagnify}
|
||||
@click=${this._showQuickBar}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown slot="actionItems" @wa-select=${this._handleMenuAction}>
|
||||
<ha-button-menu slot="actionItems" @action=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown-item value="check_updates">
|
||||
<ha-svg-icon slot="icon" .path=${mdiRefresh}></ha-svg-icon>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize("ui.panel.config.updates.check_updates")}
|
||||
</ha-dropdown-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRefresh}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-dropdown-item value="restart">
|
||||
<ha-svg-icon slot="icon" .path=${mdiPower}></ha-svg-icon>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.system_dashboard.restart_homeassistant"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPower}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
|
||||
<ha-config-section
|
||||
.narrow=${this.narrow}
|
||||
@@ -371,13 +371,12 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "check_updates":
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
checkForEntityUpdates(this, this.hass);
|
||||
break;
|
||||
case "restart":
|
||||
case 1:
|
||||
showRestartDialog(this);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiCancel,
|
||||
mdiChevronRight,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiMenuDown,
|
||||
@@ -38,15 +39,11 @@ import type {
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-devices";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
@@ -54,6 +51,9 @@ import "../../../components/ha-filter-integrations";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
import { getSubEntries, sortConfigEntries } from "../../../data/config_entries";
|
||||
@@ -702,80 +702,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
private _renderAreaItems = (submenu = false) =>
|
||||
html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="toggle_area"
|
||||
data-area=${area.area_id}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
${area.name}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="__no_area__">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="__create_area__"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
private _renderLabelItems = (submenu = false) =>
|
||||
html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((deviceId) =>
|
||||
this.hass.devices[deviceId]?.labels.includes(label.label_id)
|
||||
);
|
||||
const partial =
|
||||
!selected &&
|
||||
this._selected.some((deviceId) =>
|
||||
this.hass.devices[deviceId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
.value=${label.label_id}
|
||||
data-action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="icon"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-dropdown-item>`;
|
||||
})}
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
@click=${this._bulkCreateLabel}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { devicesOutput } = this._devicesAndFilterDomains(
|
||||
this.hass.devices,
|
||||
@@ -792,6 +718,77 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
|
||||
const areaItems = html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-md-menu-item
|
||||
.value=${area.area_id}
|
||||
.clickAction=${this._handleBulkArea}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
<div slot="headline">${area.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
|
||||
const labelItems = html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((deviceId) =>
|
||||
this.hass.devices[deviceId]?.labels.includes(label.label_id)
|
||||
);
|
||||
const partial =
|
||||
!selected &&
|
||||
this._selected.some((deviceId) =>
|
||||
this.hass.devices[deviceId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-md-menu-item
|
||||
.value=${label.label_id}
|
||||
.action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
keep-open
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="start"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-md-menu-item>`;
|
||||
})}
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</div></ha-md-menu-item
|
||||
>`;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
@@ -909,7 +906,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
></ha-filter-labels>
|
||||
|
||||
${!this.narrow
|
||||
? html`<ha-dropdown slot="selection-bar">
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -921,15 +918,12 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderLabelItems()}
|
||||
</ha-dropdown>
|
||||
${labelItems}
|
||||
</ha-md-button-menu>
|
||||
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -941,14 +935,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderAreaItems()}
|
||||
</ha-dropdown>`}`
|
||||
${areaItems}
|
||||
</ha-md-button-menu>`}`
|
||||
: nothing}
|
||||
<ha-dropdown
|
||||
has-overflow
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
<ha-md-button-menu has-overflow slot="selection-bar">
|
||||
${this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
@@ -969,32 +959,51 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
slot="trigger"
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html`<ha-dropdown-item>
|
||||
? html` <ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
${this._renderLabelItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing}
|
||||
${areasInOverflow
|
||||
? html`<ha-dropdown-item>
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
${this._renderAreaItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${areaItems}</ha-md-menu>
|
||||
</ha-sub-menu>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
||||
: nothing}
|
||||
<ha-dropdown-item
|
||||
value="delete"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._deleteSelected}
|
||||
.disabled=${!this._selectedCanDelete.length}
|
||||
variant="danger"
|
||||
class="warning"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.delete_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
@@ -1084,28 +1093,12 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleOverflowMenuSelect(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
private _handleBulkArea = (item) => {
|
||||
const area = item.value;
|
||||
this._bulkAddArea(area);
|
||||
};
|
||||
|
||||
switch (item.value) {
|
||||
case "delete":
|
||||
this._deleteSelected();
|
||||
break;
|
||||
case "__no_area__":
|
||||
this._bulkAddArea(null);
|
||||
break;
|
||||
case "__create_area__":
|
||||
this._bulkCreateArea();
|
||||
break;
|
||||
case "toggle_area":
|
||||
if (item.dataset.area) {
|
||||
this._bulkAddArea(item.dataset.area);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _bulkAddArea(area: string | null) {
|
||||
private async _bulkAddArea(area: string) {
|
||||
const promises: Promise<DeviceRegistryEntry>[] = [];
|
||||
this._selected.forEach((deviceId) => {
|
||||
promises.push(
|
||||
@@ -1140,6 +1133,12 @@ ${rejected
|
||||
});
|
||||
};
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private async _bulkLabel(label: string, action: "add" | "remove") {
|
||||
const promises: Promise<DeviceRegistryEntry>[] = [];
|
||||
this._selected.forEach((deviceId) => {
|
||||
@@ -1179,13 +1178,6 @@ ${rejected
|
||||
});
|
||||
};
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
ev.stopPropagation();
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.dataset.action;
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private _deleteSelected = () => {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
@@ -1285,7 +1277,7 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-dropdown ha-assist-chip {
|
||||
ha-md-button-menu ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
ha-label {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCancel,
|
||||
mdiChevronRight,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiEye,
|
||||
@@ -56,9 +56,8 @@ import type {
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-filter-devices";
|
||||
import "../../../components/ha-filter-domains";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
@@ -67,6 +66,9 @@ import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
@@ -747,48 +749,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
private _renderLabelItems = (submenu = false) =>
|
||||
html` ${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
const partial =
|
||||
!selected &&
|
||||
this._selected.some((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
.value=${label.label_id}
|
||||
data-action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleLabelMenuSelect}
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="icon"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-dropdown-item>`;
|
||||
})}
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
@click=${this._handleCreateLabel}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || this._entities === undefined) {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
@@ -813,13 +773,53 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
[...filteredDomains][0]
|
||||
);
|
||||
|
||||
const labelItems = html` ${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
const partial =
|
||||
!selected &&
|
||||
this._selected.some((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-md-menu-item
|
||||
.value=${label.label_id}
|
||||
.action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
keep-open
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="start"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-md-menu-item>`;
|
||||
})}
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</div></ha-md-menu-item
|
||||
>`;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config"}
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
@@ -829,14 +829,16 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
{ number: filteredEntities.length }
|
||||
)}
|
||||
has-filters
|
||||
.filters=${Object.values(this._filters).filter((filter) =>
|
||||
.filters=${
|
||||
Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter)
|
||||
? filter.length
|
||||
: filter &&
|
||||
Object.values(filter).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
).length
|
||||
}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
.initialGroupColumn=${this._activeGrouping ?? "device_full"}
|
||||
@@ -863,37 +865,32 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
slot="toolbar-icon"
|
||||
></ha-integration-overflow-menu>
|
||||
|
||||
${!this.narrow
|
||||
? html`<ha-dropdown slot="selection-bar">
|
||||
|
||||
${
|
||||
!this.narrow
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderLabelItems()}
|
||||
</ha-dropdown>`
|
||||
: nothing}
|
||||
<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
${this.narrow
|
||||
${labelItems}
|
||||
</ha-md-button-menu>`
|
||||
: nothing
|
||||
}
|
||||
<ha-md-button-menu has-overflow slot="selection-bar">
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
|
||||
</ha-assist-chip>`
|
||||
: html`<ha-icon-button
|
||||
.path=${mdiDotsVertical}
|
||||
@@ -901,65 +898,105 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html`<ha-dropdown-item>
|
||||
></ha-icon-button>`
|
||||
}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
${this._renderLabelItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
|
||||
</ha-sub-menu>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<ha-dropdown-item value="enable">
|
||||
<ha-svg-icon slot="icon" .path=${mdiToggleSwitch}></ha-svg-icon>
|
||||
<ha-md-menu-item .clickAction=${this._enableSelected}>
|
||||
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.enable_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="disable">
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._disableSelected}>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
slot="start"
|
||||
.path=${mdiToggleSwitchOffOutline}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.disable_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-dropdown-item value="unhide">
|
||||
<ha-svg-icon slot="icon" .path=${mdiEye}></ha-svg-icon>
|
||||
<ha-md-menu-item .clickAction=${this._unhideSelected}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiEye}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.unhide_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="hide">
|
||||
<ha-svg-icon slot="icon" .path=${mdiEyeOff}></ha-svg-icon>
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._hideSelected}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiEyeOff}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-dropdown-item value="restore_entity_id">
|
||||
<ha-svg-icon slot="icon" .path=${mdiRestore}></ha-svg-icon>
|
||||
<ha-md-menu-item .clickAction=${this._restoreEntityIdSelected}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiRestore}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.restore_entity_id_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-dropdown-item value="remove" variant="danger">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
<ha-md-menu-item .clickAction=${this._removeSelected} class="warning">
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.delete_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
${Array.isArray(this._filters.config_entry) &&
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
|
||||
</ha-md-button-menu>
|
||||
${
|
||||
Array.isArray(this._filters.config_entry) &&
|
||||
this._filters.config_entry.length
|
||||
? html`<ha-alert slot="filter-pane">
|
||||
${this.hass.localize(
|
||||
@@ -972,11 +1009,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
Array.isArray(this._filters.sub_entry) &&
|
||||
this._filters.sub_entry.length
|
||||
? html` (${this._subEntries?.find(
|
||||
(entry) => entry.subentry_id === this._filters.sub_entry![0]
|
||||
(entry) =>
|
||||
entry.subentry_id === this._filters.sub_entry![0]
|
||||
)?.title || this._filters.sub_entry[0]})`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<ha-filter-floor-areas
|
||||
.hass=${this.hass}
|
||||
type="entity"
|
||||
@@ -1037,16 +1076,20 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-labels>
|
||||
${includeAddDeviceFab
|
||||
${
|
||||
includeAddDeviceFab
|
||||
? html`<ha-fab
|
||||
.label=${this.hass.localize("ui.panel.config.devices.add_device")}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.devices.add_device"
|
||||
)}
|
||||
extended
|
||||
@click=${this._addDevice}
|
||||
slot="fab"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
@@ -1180,44 +1223,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleLabelMenuSelect(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const item = ev.currentTarget as HaDropdownItem;
|
||||
|
||||
const label = item.value as string;
|
||||
const action = (item as HTMLElement).dataset.action as "add" | "remove";
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private _handleCreateLabel() {
|
||||
this._bulkCreateLabel();
|
||||
}
|
||||
|
||||
private _handleOverflowMenuSelect(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
|
||||
switch (item.value) {
|
||||
case "enable":
|
||||
this._enableSelected();
|
||||
break;
|
||||
case "disable":
|
||||
this._disableSelected();
|
||||
break;
|
||||
case "unhide":
|
||||
this._unhideSelected();
|
||||
break;
|
||||
case "hide":
|
||||
this._hideSelected();
|
||||
break;
|
||||
case "remove":
|
||||
this._removeSelected();
|
||||
break;
|
||||
case "restore_entity_id":
|
||||
this._restoreEntityIdSelected();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _enableSelected = async () => {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
@@ -1341,6 +1346,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
this._clearSelection();
|
||||
};
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
await this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private async _bulkLabel(label: string, action: "add" | "remove") {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1602,7 +1613,7 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-dropdown ha-assist-chip {
|
||||
ha-md-button-menu ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
ha-label {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCancel,
|
||||
mdiChevronRight,
|
||||
mdiCog,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
@@ -45,9 +45,6 @@ import type {
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-categories";
|
||||
import "../../../components/ha-filter-devices";
|
||||
@@ -56,9 +53,8 @@ import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
@@ -358,7 +354,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
template: (helper) => helper.area || "—",
|
||||
},
|
||||
labels: {
|
||||
title: "",
|
||||
@@ -611,35 +606,42 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
private _renderCategoryItems = (submenu = false) =>
|
||||
html`${this._categories?.map(
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this.hass ||
|
||||
this._stateItems === undefined ||
|
||||
this._entityEntries === undefined ||
|
||||
this._configEntries === undefined
|
||||
) {
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
const categoryItems = html`${this._categories?.map(
|
||||
(category) =>
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_category"
|
||||
data-category=${category.category_id}
|
||||
html`<ha-md-menu-item
|
||||
.value=${category.category_id}
|
||||
.clickAction=${this._handleBulkCategory}
|
||||
>
|
||||
${category.icon
|
||||
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`}
|
||||
${category.name}
|
||||
</ha-dropdown-item>`
|
||||
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
|
||||
<div slot="headline">${category.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no_category">
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="create_category"
|
||||
>
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
private _renderLabelItems = (submenu = false) =>
|
||||
html`${this._labels?.map((label) => {
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
const labelItems = html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
this._labelsForEntity(entityId).includes(label.label_id)
|
||||
@@ -649,14 +651,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
this._selected.some((entityId) =>
|
||||
this._labelsForEntity(entityId).includes(label.label_id)
|
||||
);
|
||||
return html`<ha-dropdown-item
|
||||
@click=${this._handleLabelMenuSelect}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
return html`<ha-md-menu-item
|
||||
.value=${label.label_id}
|
||||
data-action=${selected ? "remove" : "add"}
|
||||
.action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
keep-open
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="icon"
|
||||
slot="start"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
@@ -670,25 +672,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-dropdown-item>`;
|
||||
})}<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
@click=${this._bulkCreateLabel}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
>
|
||||
</ha-md-menu-item> `;
|
||||
})}<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this.hass ||
|
||||
this._stateItems === undefined ||
|
||||
this._entityEntries === undefined ||
|
||||
this._configEntries === undefined
|
||||
) {
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
const labelsInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
@@ -790,10 +780,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
></ha-filter-categories>
|
||||
|
||||
${!this.narrow
|
||||
? html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleMenuSelect}
|
||||
>
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -805,11 +792,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderCategoryItems()}
|
||||
</ha-dropdown>
|
||||
${categoryItems}
|
||||
</ha-md-button-menu>
|
||||
${labelsInOverflow
|
||||
? nothing
|
||||
: html`<ha-dropdown slot="selection-bar">
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -821,15 +808,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderLabelItems()}
|
||||
</ha-dropdown>`}`
|
||||
${labelItems}
|
||||
</ha-md-button-menu>`}`
|
||||
: nothing}
|
||||
${this.narrow || labelsInOverflow
|
||||
? html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleMenuSelect}
|
||||
>
|
||||
${this.narrow
|
||||
? html`
|
||||
<ha-md-button-menu has-overflow slot="selection-bar">
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
@@ -847,24 +833,50 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html`<ha-dropdown-item>
|
||||
></ha-icon-button>`
|
||||
}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
)}
|
||||
${this._renderCategoryItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || this.hass.dockedSidebar === "docked"
|
||||
? html`<ha-dropdown-item>
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || this.hass.dockedSidebar === "docked"
|
||||
? html` <ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
${this._renderLabelItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
</ha-dropdown>`
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
</ha-md-button-menu>`
|
||||
: nothing}
|
||||
|
||||
<ha-integration-overflow-menu
|
||||
@@ -1007,7 +1019,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private async _bulkAddCategory(category: string | null) {
|
||||
private _handleBulkCategory = (item) => {
|
||||
const category = item.value;
|
||||
this._bulkAddCategory(category);
|
||||
};
|
||||
|
||||
private async _bulkAddCategory(category: string) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(
|
||||
@@ -1032,6 +1049,12 @@ ${rejected
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private async _bulkLabel(label: string, action: "add" | "remove") {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1067,32 +1090,6 @@ ${rejected
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleMenuSelect(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "no_category":
|
||||
this._bulkAddCategory(null);
|
||||
break;
|
||||
case "create_category":
|
||||
this._bulkCreateCategory();
|
||||
break;
|
||||
case "move_category":
|
||||
if (item.dataset.category) {
|
||||
this._bulkAddCategory(item.dataset.category);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleLabelMenuSelect(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const item = ev.currentTarget as HaDropdownItem;
|
||||
|
||||
const label = item.value as string;
|
||||
const action = (item as HTMLElement).dataset.action as "add" | "remove";
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
@@ -1433,7 +1430,7 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-dropdown ha-assist-chip {
|
||||
ha-md-button-menu ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
ha-label {
|
||||
|
||||
@@ -4,10 +4,8 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -502,18 +500,15 @@ export class HassioNetwork extends LitElement {
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<ha-dropdown
|
||||
@wa-show=${this._handleDNSMenuOpened}
|
||||
@wa-hide=${this._handleDNSMenuClosed}
|
||||
@wa-select=${this._handleDNSMenuSelect}
|
||||
<ha-button-menu
|
||||
@opened=${this._handleDNSMenuOpened}
|
||||
@closed=${this._handleDNSMenuClosed}
|
||||
.version=${version}
|
||||
>
|
||||
<ha-button
|
||||
class="add-nameserver"
|
||||
appearance="filled"
|
||||
size="small"
|
||||
slot="trigger"
|
||||
class="add-nameserver"
|
||||
>
|
||||
<ha-button appearance="filled" size="small" slot="trigger">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.add_dns_server"
|
||||
)}
|
||||
@@ -524,21 +519,21 @@ export class HassioNetwork extends LitElement {
|
||||
</ha-button>
|
||||
${Object.entries(PREDEFINED_DNS[version]).map(
|
||||
([name, addresses]) => html`
|
||||
<ha-dropdown-item
|
||||
value="predefined"
|
||||
<ha-list-item
|
||||
@click=${this._addPredefinedDNS}
|
||||
.version=${version}
|
||||
.addresses=${addresses}
|
||||
>
|
||||
${name}
|
||||
</ha-dropdown-item>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
<ha-dropdown-item value="custom" .version=${version}>
|
||||
<ha-list-item @click=${this._addCustomDNS} .version=${version}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.custom_dns"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
@@ -752,23 +747,21 @@ export class HassioNetwork extends LitElement {
|
||||
this._dnsMenuOpen = false;
|
||||
}
|
||||
|
||||
private _handleDNSMenuSelect(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem & {
|
||||
version: "ipv4" | "ipv6";
|
||||
addresses?: string[];
|
||||
};
|
||||
const version = item.version;
|
||||
|
||||
if (item.value === "predefined" && item.addresses) {
|
||||
private _addPredefinedDNS(ev: Event) {
|
||||
const source = ev.target as any;
|
||||
const version = source.version as "ipv4" | "ipv6";
|
||||
const addresses = source.addresses as string[];
|
||||
if (!this._interface![version]!.nameservers) {
|
||||
this._interface![version]!.nameservers = [];
|
||||
}
|
||||
this._interface![version]!.nameservers!.push(...item.addresses);
|
||||
this._interface![version]!.nameservers!.push(...addresses);
|
||||
this._dirty = true;
|
||||
this.requestUpdate("_interface");
|
||||
return;
|
||||
}
|
||||
if (item.value === "custom") {
|
||||
|
||||
private _addCustomDNS(ev: Event) {
|
||||
const source = ev.target as any;
|
||||
const version = source.version as "ipv4" | "ipv6";
|
||||
if (!this._interface![version]!.nameservers) {
|
||||
this._interface![version]!.nameservers = [];
|
||||
}
|
||||
@@ -776,7 +769,6 @@ export class HassioNetwork extends LitElement {
|
||||
this._dirty = true;
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
}
|
||||
|
||||
private _removeNameserver(ev: Event): void {
|
||||
const source = ev.target as any;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronRight,
|
||||
mdiCog,
|
||||
mdiContentDuplicate,
|
||||
mdiDelete,
|
||||
@@ -45,9 +45,6 @@ import type {
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-categories";
|
||||
import "../../../components/ha-filter-devices";
|
||||
@@ -56,7 +53,11 @@ import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
@@ -433,8 +434,34 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
private _renderLabelItems = (submenu = false) =>
|
||||
html` ${this._labels?.map((label) => {
|
||||
protected render(): TemplateResult {
|
||||
const categoryItems = html`${this._categories?.map(
|
||||
(category) =>
|
||||
html`<ha-md-menu-item
|
||||
.value=${category.category_id}
|
||||
.clickAction=${this._handleBulkCategory}
|
||||
>
|
||||
${category.icon
|
||||
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
|
||||
<div slot="headline">${category.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
|
||||
const labelItems = html` ${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
@@ -444,14 +471,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
this._selected.some((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value=${label.label_id}
|
||||
data-action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleLabelMenuSelect}
|
||||
return html`<ha-md-menu-item
|
||||
.value=${label.label_id}
|
||||
.action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
keep-open
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="icon"
|
||||
slot="start"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
@@ -465,74 +492,46 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-dropdown-item>`;
|
||||
</ha-md-menu-item>`;
|
||||
})}
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
@click=${this._handleCreateLabel}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="create-label"
|
||||
>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</ha-dropdown-item>`;
|
||||
</div></ha-md-menu-item
|
||||
>`;
|
||||
|
||||
private _renderCategoryItems = (submenu = false) =>
|
||||
html`${this._categories?.map(
|
||||
(category) =>
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_category"
|
||||
data-category=${category.category_id}
|
||||
>
|
||||
${category.icon
|
||||
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`}
|
||||
${category.name}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no-category">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="create-category"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
private _renderAreaItems = (submenu = false) =>
|
||||
html`${Object.values(this.hass.areas).map(
|
||||
const areaItems = html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_area"
|
||||
data-area=${area.area_id}
|
||||
html`<ha-md-menu-item
|
||||
.value=${area.area_id}
|
||||
.clickAction=${this._handleBulkArea}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="icon"
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
${area.name}
|
||||
</ha-dropdown-item>`
|
||||
<div slot="headline">${area.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no-area">
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="create-area">
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</ha-dropdown-item>`;
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const areasInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 900) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
@@ -655,10 +654,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
></ha-filter-categories>
|
||||
|
||||
${!this.narrow
|
||||
? html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -670,11 +666,11 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderCategoryItems()}
|
||||
</ha-dropdown>
|
||||
${categoryItems}
|
||||
</ha-md-button-menu>
|
||||
${labelsInOverflow
|
||||
? nothing
|
||||
: html`<ha-dropdown slot="selection-bar">
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -686,14 +682,11 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderLabelItems()}
|
||||
</ha-dropdown>`}
|
||||
${labelItems}
|
||||
</ha-md-button-menu>`}
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -705,15 +698,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderAreaItems()}
|
||||
</ha-dropdown>`}`
|
||||
${areaItems}
|
||||
</ha-md-button-menu>`}`
|
||||
: nothing}
|
||||
${this.narrow || areasInOverflow
|
||||
? html` <ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
${this.narrow
|
||||
? html`
|
||||
<ha-md-button-menu has-overflow slot="selection-bar">
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
@@ -731,32 +723,68 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html`<ha-dropdown-item>
|
||||
></ha-icon-button>`
|
||||
}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
)}
|
||||
${this._renderCategoryItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || labelsInOverflow
|
||||
? html`<ha-dropdown-item>
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || labelsInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
${this._renderLabelItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || areasInOverflow
|
||||
? html`<ha-dropdown-item>
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || areasInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
${this._renderAreaItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
</ha-dropdown>`
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${areaItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
</ha-md-button-menu>`
|
||||
: nothing}
|
||||
${!this.scenes.length
|
||||
? html`<div class="empty" slot="empty">
|
||||
@@ -927,52 +955,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleLabelMenuSelect = (ev: CustomEvent) => {
|
||||
ev.stopPropagation();
|
||||
const item = ev.currentTarget as HaDropdownItem & {
|
||||
dataset: { action?: string };
|
||||
};
|
||||
const action = item.dataset.action as "add" | "remove";
|
||||
this._bulkLabel(item.value, action);
|
||||
private _handleBulkCategory = (item) => {
|
||||
const category = item.value;
|
||||
this._bulkAddCategory(category);
|
||||
};
|
||||
|
||||
private _handleCreateLabel = () => {
|
||||
this._bulkCreateLabel();
|
||||
};
|
||||
|
||||
private _handleOverflowMenuSelect = (ev: CustomEvent) => {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
|
||||
switch (item.value) {
|
||||
case "create-category":
|
||||
this._bulkCreateCategory();
|
||||
break;
|
||||
case "no-category":
|
||||
this._bulkAddCategory(null);
|
||||
break;
|
||||
case "create-label":
|
||||
this._bulkCreateLabel();
|
||||
break;
|
||||
case "create-area":
|
||||
this._bulkCreateArea();
|
||||
break;
|
||||
case "no-area":
|
||||
this._bulkAddArea(null);
|
||||
break;
|
||||
case "move_category":
|
||||
if (item.dataset.category) {
|
||||
this._bulkAddCategory(item.dataset.category);
|
||||
}
|
||||
break;
|
||||
case "move_area":
|
||||
if (item.dataset.area) {
|
||||
this._bulkAddArea(item.dataset.area);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private async _bulkAddCategory(category: string | null) {
|
||||
private async _bulkAddCategory(category: string) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(
|
||||
@@ -997,6 +985,12 @@ ${rejected
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private async _bulkLabel(label: string, action: "add" | "remove") {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1027,7 +1021,12 @@ ${rejected
|
||||
}
|
||||
}
|
||||
|
||||
private async _bulkAddArea(area: string | null) {
|
||||
private _handleBulkArea = (item) => {
|
||||
const area = item.value;
|
||||
this._bulkAddArea(area);
|
||||
};
|
||||
|
||||
private async _bulkAddArea(area: string) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(
|
||||
@@ -1232,7 +1231,7 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-dropdown ha-assist-chip {
|
||||
ha-md-button-menu ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
ha-label {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import {
|
||||
mdiCog,
|
||||
mdiContentDuplicate,
|
||||
@@ -32,10 +32,8 @@ import "../../../components/entity/ha-entities-picker";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-picker";
|
||||
@@ -229,66 +227,78 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
? computeStateName(this._scene)
|
||||
: this.hass.localize("ui.panel.config.scene.editor.default_name")}
|
||||
>
|
||||
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
|
||||
<ha-button-menu
|
||||
slot="toolbar-icon"
|
||||
@action=${this._handleMenuAction}
|
||||
activatable
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown-item
|
||||
value="apply"
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${!this.sceneId || this._mode === "live"}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.apply")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="info" .disabled=${!this.sceneId}>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.show_info")}
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
slot="graphic"
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.show_info")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="settings" .disabled=${!this.sceneId}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_settings"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-dropdown-item value="category" .disabled=${!this.sceneId}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
|
||||
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.scene.picker.${this._getCategory(this._entityRegistryEntries, this._scene?.entity_id) ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiTag}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-dropdown-item value="toggle_yaml">
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${this._mode !== "yaml" ? "yaml" : "ui"}`
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
<li divider role="separator"></li>
|
||||
|
||||
<ha-dropdown-item value="duplicate" .disabled=${!this.sceneId}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentDuplicate}></ha-svg-icon>
|
||||
<ha-list-item .disabled=${!this.sceneId} graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.duplicate_scene"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-dropdown-item
|
||||
value="delete"
|
||||
<ha-list-item
|
||||
.disabled=${!this.sceneId}
|
||||
.variant=${this.sceneId ? "danger" : "default"}
|
||||
class=${classMap({ warning: Boolean(this.sceneId) })}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.delete_scene")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<ha-svg-icon
|
||||
class=${classMap({ warning: Boolean(this.sceneId) })}
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
${this._errors ? html` <div class="errors">${this._errors}</div> ` : ""}
|
||||
${this._mode === "yaml" ? this._renderYamlMode() : this._renderUiMode()}
|
||||
<ha-fab
|
||||
@@ -642,25 +652,24 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "apply":
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
activateScene(this.hass, this._scene!.entity_id);
|
||||
break;
|
||||
case "info":
|
||||
case 1:
|
||||
fireEvent(this, "hass-more-info", { entityId: this._scene!.entity_id });
|
||||
break;
|
||||
case "settings":
|
||||
case 2:
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: this._scene!.entity_id,
|
||||
view: "settings",
|
||||
});
|
||||
break;
|
||||
case "category":
|
||||
case 3:
|
||||
this._editCategory(this._scene!);
|
||||
break;
|
||||
case "toggle_yaml":
|
||||
case 4:
|
||||
if (this._mode === "yaml") {
|
||||
this._initEntities(this._config!);
|
||||
this._exitYamlMode();
|
||||
@@ -668,10 +677,10 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
this._enterYamlMode();
|
||||
}
|
||||
break;
|
||||
case "duplicate":
|
||||
case 5:
|
||||
this._duplicate();
|
||||
break;
|
||||
case "delete":
|
||||
case 6:
|
||||
this._deleteTapped();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronRight,
|
||||
mdiCog,
|
||||
mdiContentDuplicate,
|
||||
mdiDelete,
|
||||
@@ -46,9 +46,6 @@ import type {
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-blueprints";
|
||||
import "../../../components/ha-filter-categories";
|
||||
@@ -58,6 +55,10 @@ import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
@@ -418,35 +419,33 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
private _renderCategoryItems = (submenu = false) =>
|
||||
html`${this._categories?.map(
|
||||
protected render(): TemplateResult {
|
||||
const categoryItems = html`${this._categories?.map(
|
||||
(category) =>
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_category"
|
||||
data-category=${category.category_id}
|
||||
html`<ha-md-menu-item
|
||||
.value=${category.category_id}
|
||||
.clickAction=${this._handleBulkCategory}
|
||||
>
|
||||
${category.icon
|
||||
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`}
|
||||
${category.name}
|
||||
</ha-dropdown-item>`
|
||||
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
|
||||
<div slot="headline">${category.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no-category">
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="create-category"
|
||||
>
|
||||
</div> </ha-md-menu-item
|
||||
><ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</ha-dropdown-item>`;
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
|
||||
private _renderLabelItems = (submenu = false) =>
|
||||
html`${this._labels?.map((label) => {
|
||||
const labelItems = html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
@@ -456,13 +455,15 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
this._selected.some((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-dropdown-item
|
||||
@click=${this._handleLabelMenuSelect}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value=${label.label_id}
|
||||
data-action=${selected ? "remove" : "add"}
|
||||
><ha-checkbox
|
||||
slot="icon"
|
||||
return html`<ha-md-menu-item
|
||||
.value=${label.label_id}
|
||||
.action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
keep-open
|
||||
reducedTouchTarget
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="start"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
></ha-checkbox>
|
||||
@@ -475,47 +476,46 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-dropdown-item>`;
|
||||
</ha-md-menu-item>`;
|
||||
})}
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
@click=${this._bulkCreateLabel}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="create-label"
|
||||
>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</ha-dropdown-item>`;
|
||||
</div></ha-md-menu-item
|
||||
>`;
|
||||
|
||||
private _renderAreaItems = (submenu = false) =>
|
||||
html`${Object.values(this.hass.areas).map(
|
||||
const areaItems = html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_area"
|
||||
data-area=${area.area_id}
|
||||
html`<ha-md-menu-item
|
||||
.value=${area.area_id}
|
||||
.clickAction=${this._handleBulkArea}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="icon"
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
${area.name}
|
||||
</ha-dropdown-item>`
|
||||
<div slot="headline">${area.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no-area">
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="create-area">
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</ha-dropdown-item>`;
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const areasInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 900) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
@@ -647,10 +647,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
></ha-filter-blueprints>
|
||||
|
||||
${!this.narrow
|
||||
? html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -662,11 +659,11 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderCategoryItems()}
|
||||
</ha-dropdown>
|
||||
${categoryItems}
|
||||
</ha-md-button-menu>
|
||||
${labelsInOverflow
|
||||
? nothing
|
||||
: html`<ha-dropdown slot="selection-bar">
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -678,14 +675,11 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderLabelItems()}
|
||||
</ha-dropdown>`}
|
||||
${labelItems}
|
||||
</ha-md-button-menu>`}
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -697,15 +691,14 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderAreaItems()}
|
||||
</ha-dropdown>`}`
|
||||
${areaItems}
|
||||
</ha-md-button-menu>`}`
|
||||
: nothing}
|
||||
${this.narrow || areasInOverflow
|
||||
? html` <ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
${this.narrow
|
||||
? html`
|
||||
<ha-md-button-menu has-overflow slot="selection-bar">
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
@@ -723,32 +716,68 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html`<ha-dropdown-item>
|
||||
></ha-icon-button>`
|
||||
}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
)}
|
||||
${this._renderCategoryItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || labelsInOverflow
|
||||
? html`<ha-dropdown-item>
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || labelsInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
${this._renderLabelItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || areasInOverflow
|
||||
? html`<ha-dropdown-item>
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || areasInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
${this._renderAreaItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
</ha-dropdown>`
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${areaItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
</ha-md-button-menu>`
|
||||
: nothing}
|
||||
${!this.scripts.length
|
||||
? html` <div class="empty" slot="empty">
|
||||
@@ -966,46 +995,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleLabelMenuSelect = (ev: CustomEvent) => {
|
||||
ev.stopPropagation();
|
||||
const item = ev.currentTarget as HaDropdownItem & {
|
||||
dataset: { action?: string };
|
||||
};
|
||||
const action = item.dataset.action as "add" | "remove";
|
||||
this._bulkLabel(item.value, action);
|
||||
};
|
||||
|
||||
private _handleOverflowMenuSelect = (ev: CustomEvent) => {
|
||||
const item = ev.detail.item as HaDropdownItem & {
|
||||
dataset: { action?: string };
|
||||
};
|
||||
switch (item.value) {
|
||||
case "create-category":
|
||||
this._bulkCreateCategory();
|
||||
break;
|
||||
case "no-category":
|
||||
this._bulkAddCategory(null!);
|
||||
break;
|
||||
case "create-label":
|
||||
this._bulkCreateLabel();
|
||||
break;
|
||||
case "create-area":
|
||||
this._bulkCreateArea();
|
||||
break;
|
||||
case "no-area":
|
||||
this._bulkAddArea(null!);
|
||||
break;
|
||||
case "move_area":
|
||||
if (item.dataset.area) {
|
||||
this._bulkAddArea(item.dataset.area);
|
||||
}
|
||||
break;
|
||||
case "move_category":
|
||||
if (item.dataset.category) {
|
||||
this._bulkAddCategory(item.dataset.category);
|
||||
}
|
||||
break;
|
||||
}
|
||||
private _handleBulkCategory = (item) => {
|
||||
const category = item.value;
|
||||
this._bulkAddCategory(category);
|
||||
};
|
||||
|
||||
private async _bulkAddCategory(category: string) {
|
||||
@@ -1033,6 +1025,12 @@ ${rejected
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private async _bulkLabel(label: string, action: "add" | "remove") {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1237,6 +1235,11 @@ ${rejected
|
||||
});
|
||||
};
|
||||
|
||||
private _handleBulkArea = (item) => {
|
||||
const area = item.value;
|
||||
this._bulkAddArea(area);
|
||||
};
|
||||
|
||||
private async _bulkAddArea(area: string) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1319,7 +1322,7 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-dropdown ha-assist-chip {
|
||||
ha-md-button-menu ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
ha-label {
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getGraphColorByIndex } from "../../../common/color/colors";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
|
||||
@@ -42,10 +44,10 @@ import {
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { roundWithOneDecimal } from "../../../util/calculate";
|
||||
import "../core/ha-config-analytics";
|
||||
import { showMoveDatadiskDialog } from "./show-dialog-move-datadisk";
|
||||
import { showMountViewDialog } from "./show-dialog-view-mount";
|
||||
import "./storage-breakdown-chart";
|
||||
|
||||
@customElement("ha-config-section-storage")
|
||||
class HaConfigSectionStorage extends LitElement {
|
||||
@@ -102,11 +104,10 @@ class HaConfigSectionStorage extends LitElement {
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<storage-breakdown-chart
|
||||
.hass=${this.hass}
|
||||
.hostInfo=${this._hostInfo}
|
||||
.storageInfo=${this._storageInfo}
|
||||
></storage-breakdown-chart>
|
||||
${this._renderStorageMetrics(
|
||||
this._hostInfo,
|
||||
this._storageInfo
|
||||
)}
|
||||
${this._renderDiskLifeTime(this._hostInfo.disk_life_time)}
|
||||
</div>
|
||||
${this._hostInfo
|
||||
@@ -268,6 +269,95 @@ class HaConfigSectionStorage extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderStorageMetrics = memoizeOne(
|
||||
(hostInfo?: HassioHostInfo, storageInfo?: HostDisksUsage | null) => {
|
||||
if (!hostInfo) {
|
||||
return nothing;
|
||||
}
|
||||
const computedStyles = getComputedStyle(this);
|
||||
let totalSpaceGB = hostInfo.disk_total;
|
||||
let usedSpaceGB = hostInfo.disk_used;
|
||||
// hostInfo.disk_free is sometimes 0, so we may need to calculate it
|
||||
let freeSpaceGB =
|
||||
hostInfo.disk_free || hostInfo.disk_total - hostInfo.disk_used;
|
||||
const segments: Segment[] = [];
|
||||
if (storageInfo) {
|
||||
const totalSpace =
|
||||
storageInfo.total_bytes ?? this._gbToBytes(hostInfo.disk_total);
|
||||
totalSpaceGB = this._bytesToGB(totalSpace);
|
||||
usedSpaceGB = this._bytesToGB(storageInfo.used_bytes);
|
||||
freeSpaceGB = this._bytesToGB(totalSpace - storageInfo.used_bytes);
|
||||
storageInfo.children?.forEach((child, index) => {
|
||||
if (child.used_bytes > 0) {
|
||||
const space = this._bytesToGB(child.used_bytes);
|
||||
segments.push({
|
||||
value: space,
|
||||
color: getGraphColorByIndex(index, computedStyles),
|
||||
label: html`${this.hass.localize(
|
||||
`ui.panel.config.storage.segments.${child.id}`
|
||||
) ||
|
||||
child.label ||
|
||||
child.id}
|
||||
<span style="color: var(--secondary-text-color)"
|
||||
>${roundWithOneDecimal(space)} GB</span
|
||||
>`,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
segments.push({
|
||||
value: usedSpaceGB,
|
||||
color: "var(--primary-color)",
|
||||
label: html`${this.hass.localize(
|
||||
"ui.panel.config.storage.segments.used"
|
||||
)}
|
||||
<span style="color: var(--secondary-text-color)"
|
||||
>${roundWithOneDecimal(usedSpaceGB)} GB</span
|
||||
>`,
|
||||
});
|
||||
}
|
||||
segments.push({
|
||||
value: freeSpaceGB,
|
||||
color:
|
||||
"var(--ha-bar-background-color, var(--secondary-background-color))",
|
||||
label: html`${this.hass.localize(
|
||||
"ui.panel.config.storage.segments.free"
|
||||
)}
|
||||
<span style="color: var(--secondary-text-color)"
|
||||
>${roundWithOneDecimal(freeSpaceGB)} GB</span
|
||||
>`,
|
||||
});
|
||||
return html`<ha-segmented-bar
|
||||
.heading=${this.hass.localize("ui.panel.config.storage.used_space")}
|
||||
.description=${this.hass.localize(
|
||||
"ui.panel.config.storage.detailed_description",
|
||||
{
|
||||
used: `${roundWithOneDecimal(usedSpaceGB)} GB`,
|
||||
total: `${roundWithOneDecimal(totalSpaceGB)} GB`,
|
||||
}
|
||||
)}
|
||||
.segments=${segments}
|
||||
></ha-segmented-bar>
|
||||
|
||||
${!storageInfo || storageInfo === null
|
||||
? html`<ha-alert alert-type="info">
|
||||
<ha-spinner slot="icon"></ha-spinner>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.storage.loading_detailed"
|
||||
)}</ha-alert
|
||||
>`
|
||||
: nothing}`;
|
||||
}
|
||||
);
|
||||
|
||||
private _bytesToGB(bytes: number) {
|
||||
return bytes / 1024 / 1024 / 1024;
|
||||
}
|
||||
|
||||
private _gbToBytes(GB: number) {
|
||||
return GB * 1024 * 1024 * 1024;
|
||||
}
|
||||
|
||||
private async _load() {
|
||||
this._loadStorageInfo();
|
||||
try {
|
||||
@@ -433,6 +523,10 @@ class HaConfigSectionStorage extends LitElement {
|
||||
ha-alert {
|
||||
--ha-alert-icon-size: 24px;
|
||||
}
|
||||
|
||||
ha-alert ha-spinner {
|
||||
--ha-spinner-size: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
import { mdiChartDonutVariant, mdiViewArray } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getGraphColorByIndex } from "../../../common/color/colors";
|
||||
import "../../../components/chart/ha-sunburst-chart";
|
||||
import type { SunburstNode } from "../../../components/chart/ha-sunburst-chart";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-segmented-bar";
|
||||
import "../../../components/ha-spinner";
|
||||
import type { Segment } from "../../../components/ha-segmented-bar";
|
||||
import type { HassioHostInfo, HostDisksUsage } from "../../../data/hassio/host";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { roundWithOneDecimal } from "../../../util/calculate";
|
||||
|
||||
@customElement("storage-breakdown-chart")
|
||||
export class StorageBreakdownChart extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public hostInfo?: HassioHostInfo;
|
||||
|
||||
@property({ attribute: false })
|
||||
public storageInfo?: HostDisksUsage | null;
|
||||
|
||||
@state()
|
||||
private _chartType: "bar" | "sunburst" = "bar";
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this.hostInfo) {
|
||||
return nothing;
|
||||
}
|
||||
const { totalSpaceGB, usedSpaceGB, freeSpaceGB } = this._computeSpaceValues(
|
||||
this.hostInfo,
|
||||
this.storageInfo
|
||||
);
|
||||
|
||||
const hasChildren = Boolean(this.storageInfo?.children?.length);
|
||||
const heading = this.hass.localize("ui.panel.config.storage.used_space");
|
||||
const description = this.hass.localize(
|
||||
"ui.panel.config.storage.detailed_description",
|
||||
{
|
||||
used: `${roundWithOneDecimal(usedSpaceGB)} GB`,
|
||||
total: `${roundWithOneDecimal(totalSpaceGB)} GB`,
|
||||
}
|
||||
);
|
||||
const showBarChart = this._chartType === "bar" || !hasChildren;
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="heading-text">
|
||||
<span class="heading">${heading}</span>
|
||||
<span class="description">${description}</span>
|
||||
</div>
|
||||
${hasChildren
|
||||
? html`<ha-icon-button
|
||||
.path=${this._chartType === "sunburst"
|
||||
? mdiViewArray
|
||||
: mdiChartDonutVariant}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.storage.change_chart_type"
|
||||
)}
|
||||
@click=${this._handleChartTypeChange}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<div class="chart-container ${this._chartType}">
|
||||
${showBarChart
|
||||
? html`<ha-segmented-bar
|
||||
.heading=${""}
|
||||
.segments=${this._computeSegments(
|
||||
this.storageInfo,
|
||||
usedSpaceGB,
|
||||
freeSpaceGB
|
||||
)}
|
||||
></ha-segmented-bar>`
|
||||
: html`<ha-sunburst-chart
|
||||
.hass=${this.hass}
|
||||
.data=${this._transformToSunburstData(this.storageInfo!)}
|
||||
.valueFormatter=${this._formatBytes}
|
||||
></ha-sunburst-chart>`}
|
||||
</div>
|
||||
|
||||
${!this.storageInfo || this.storageInfo === null
|
||||
? html`<ha-alert alert-type="info">
|
||||
<ha-spinner slot="icon"></ha-spinner>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.storage.loading_detailed"
|
||||
)}</ha-alert
|
||||
>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleChartTypeChange(): void {
|
||||
this._chartType = this._chartType === "bar" ? "sunburst" : "bar";
|
||||
}
|
||||
|
||||
private _computeSpaceValues = memoizeOne(
|
||||
(
|
||||
hostInfo: HassioHostInfo,
|
||||
storageInfo: HostDisksUsage | null | undefined
|
||||
) => {
|
||||
let totalSpaceGB = hostInfo.disk_total;
|
||||
let usedSpaceGB = hostInfo.disk_used;
|
||||
let freeSpaceGB =
|
||||
hostInfo.disk_free || hostInfo.disk_total - hostInfo.disk_used;
|
||||
|
||||
if (storageInfo) {
|
||||
const totalSpace =
|
||||
storageInfo.total_bytes ?? this._gbToBytes(hostInfo.disk_total);
|
||||
totalSpaceGB = this._bytesToGB(totalSpace);
|
||||
usedSpaceGB = this._bytesToGB(storageInfo.used_bytes);
|
||||
freeSpaceGB = this._bytesToGB(totalSpace - storageInfo.used_bytes);
|
||||
}
|
||||
|
||||
return { totalSpaceGB, usedSpaceGB, freeSpaceGB };
|
||||
}
|
||||
);
|
||||
|
||||
private _computeSegments = memoizeOne(
|
||||
(
|
||||
storageInfo: HostDisksUsage | null | undefined,
|
||||
usedSpaceGB: number,
|
||||
freeSpaceGB: number
|
||||
): Segment[] => {
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const segments: Segment[] = [];
|
||||
|
||||
if (storageInfo) {
|
||||
storageInfo.children?.forEach((child, index) => {
|
||||
if (child.used_bytes > 0) {
|
||||
const space = this._bytesToGB(child.used_bytes);
|
||||
segments.push({
|
||||
value: space,
|
||||
color: getGraphColorByIndex(index, computedStyles),
|
||||
label: html`${this.hass.localize(
|
||||
`ui.panel.config.storage.segments.${child.id}`
|
||||
) ||
|
||||
child.label ||
|
||||
child.id}
|
||||
<span style="color: var(--secondary-text-color)"
|
||||
>${roundWithOneDecimal(space)} GB</span
|
||||
>`,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
segments.push({
|
||||
value: usedSpaceGB,
|
||||
color: "var(--primary-color)",
|
||||
label: html`${this.hass.localize(
|
||||
"ui.panel.config.storage.segments.used"
|
||||
)}
|
||||
<span style="color: var(--secondary-text-color)"
|
||||
>${roundWithOneDecimal(usedSpaceGB)} GB</span
|
||||
>`,
|
||||
});
|
||||
}
|
||||
|
||||
segments.push({
|
||||
value: freeSpaceGB,
|
||||
color:
|
||||
"var(--ha-bar-background-color, var(--secondary-background-color))",
|
||||
label: html`${this.hass.localize(
|
||||
"ui.panel.config.storage.segments.free"
|
||||
)}
|
||||
<span style="color: var(--secondary-text-color)"
|
||||
>${roundWithOneDecimal(freeSpaceGB)} GB</span
|
||||
>`,
|
||||
});
|
||||
|
||||
return segments;
|
||||
}
|
||||
);
|
||||
|
||||
private _transformToSunburstData = memoizeOne(
|
||||
(storageInfo: HostDisksUsage): SunburstNode => {
|
||||
const transform = (
|
||||
node: HostDisksUsage,
|
||||
parentNode?: HostDisksUsage
|
||||
): SunburstNode => ({
|
||||
// prefix with parent id to avoid duplicate ids
|
||||
id: parentNode ? `${parentNode.id}.${node.id}` : node.id,
|
||||
name: this._formatLabel(node.id) || node.label,
|
||||
value: node.used_bytes,
|
||||
children: node.children?.map((child) => transform(child, node)),
|
||||
});
|
||||
return transform(storageInfo);
|
||||
}
|
||||
);
|
||||
|
||||
private _formatBytes = (bytes: number): string => {
|
||||
const gb = this._bytesToGB(bytes);
|
||||
return `${roundWithOneDecimal(gb)} GB`;
|
||||
};
|
||||
|
||||
private _formatLabel = (id: string): string =>
|
||||
this.hass.localize(`ui.panel.config.storage.segments.${id}`) || id;
|
||||
|
||||
private _bytesToGB(bytes: number): number {
|
||||
return bytes / 1024 / 1024 / 1024;
|
||||
}
|
||||
|
||||
private _gbToBytes(GB: number): number {
|
||||
return GB * 1024 * 1024 * 1024;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.heading-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-weight: 500;
|
||||
font-size: var(--ha-font-size-m);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
transition: height var(--ha-animation-base-duration) ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-container.bar {
|
||||
height: calc-size(auto, size);
|
||||
}
|
||||
|
||||
.chart-container.sunburst {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
ha-segmented-bar {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-sunburst-chart {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
ha-segmented-bar,
|
||||
ha-sunburst-chart {
|
||||
animation: fade-in var(--ha-animation-base-duration) ease;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
--ha-alert-icon-size: 24px;
|
||||
}
|
||||
|
||||
ha-alert ha-spinner {
|
||||
--ha-spinner-size: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"storage-breakdown-chart": StorageBreakdownChart;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
mdiBug,
|
||||
mdiCommentProcessingOutline,
|
||||
mdiContentDuplicate,
|
||||
mdiDotsVertical,
|
||||
mdiHelpCircle,
|
||||
mdiPlus,
|
||||
@@ -190,17 +189,6 @@ export class AssistPref extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiBug}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.id=${pipeline.id}
|
||||
@request-selected=${this._duplicatePipeline}
|
||||
>
|
||||
${this.hass.localize("ui.common.duplicate")}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
class="danger"
|
||||
graphic="icon"
|
||||
@@ -306,30 +294,6 @@ export class AssistPref extends LitElement {
|
||||
navigate(`/config/voice-assistants/debug/${id}`);
|
||||
}
|
||||
|
||||
private async _duplicatePipeline(ev: Event) {
|
||||
const id = (ev.currentTarget as HTMLElement).id as string;
|
||||
const pipeline = this._pipelines.find((res) => res.id === id);
|
||||
if (!pipeline) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.pipeline.duplicate.error_pipeline_not_found"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { id: _id, ...pipelineWithoutId } = pipeline;
|
||||
const newPipeline = {
|
||||
...pipelineWithoutId,
|
||||
name: this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.pipeline.duplicate.name",
|
||||
{ name: pipeline.name }
|
||||
),
|
||||
};
|
||||
|
||||
this._openDialog(newPipeline);
|
||||
}
|
||||
|
||||
private async _deletePipeline(ev) {
|
||||
const id = ev.currentTarget.id as string;
|
||||
if (this._preferred === id) {
|
||||
@@ -373,9 +337,7 @@ export class AssistPref extends LitElement {
|
||||
this._openDialog();
|
||||
}
|
||||
|
||||
private async _openDialog(
|
||||
pipeline?: AssistPipeline | Omit<AssistPipeline, "id">
|
||||
): Promise<void> {
|
||||
private async _openDialog(pipeline?: AssistPipeline): Promise<void> {
|
||||
showVoiceAssistantPipelineDetailDialog(this, {
|
||||
cloudActiveSubscription:
|
||||
this.cloudStatus?.logged_in && this.cloudStatus.active_subscription,
|
||||
@@ -384,21 +346,16 @@ export class AssistPref extends LitElement {
|
||||
const created = await createAssistPipeline(this.hass!, values);
|
||||
this._pipelines = this._pipelines!.concat(created);
|
||||
},
|
||||
...(pipeline && "id" in pipeline
|
||||
? {
|
||||
updatePipeline: async (values) => {
|
||||
const updated = await updateAssistPipeline(
|
||||
this.hass,
|
||||
pipeline.id,
|
||||
this.hass!,
|
||||
pipeline!.id,
|
||||
values
|
||||
);
|
||||
const pipelineToUpdate = pipeline as AssistPipeline;
|
||||
this._pipelines = this._pipelines!.map((res) =>
|
||||
res.id === pipelineToUpdate.id ? updated : res
|
||||
res === pipeline ? updated : res
|
||||
);
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,11 +48,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
this._error = undefined;
|
||||
this._cloudActive = this._params.cloudActiveSubscription;
|
||||
|
||||
if (
|
||||
this._params.pipeline &&
|
||||
"id" in this._params.pipeline &&
|
||||
this._params.pipeline.id
|
||||
) {
|
||||
if (this._params.pipeline) {
|
||||
this._data = { prefer_local_intents: false, ...this._params.pipeline };
|
||||
|
||||
this._hideWakeWord =
|
||||
@@ -83,15 +79,11 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
}
|
||||
}
|
||||
this._data = {
|
||||
...(this._params.pipeline || {}),
|
||||
language:
|
||||
this._params.pipeline?.language ||
|
||||
(this.hass.config.language || this.hass.locale.language).substring(
|
||||
0,
|
||||
2
|
||||
),
|
||||
stt_engine: this._params.pipeline?.stt_engine || sstDefault,
|
||||
tts_engine: this._params.pipeline?.tts_engine || ttsDefault,
|
||||
language: (
|
||||
this.hass.config.language || this.hass.locale.language
|
||||
).substring(0, 2),
|
||||
stt_engine: sstDefault,
|
||||
tts_engine: ttsDefault,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,13 +112,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const isExistingPipeline =
|
||||
this._params.pipeline &&
|
||||
"id" in this._params.pipeline &&
|
||||
!!this._params.pipeline.id;
|
||||
|
||||
const title =
|
||||
isExistingPipeline && this._params.pipeline?.name
|
||||
const title = this._params.pipeline?.id
|
||||
? this._params.pipeline.name
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.pipeline.detail.add_assistant_title"
|
||||
@@ -180,7 +166,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
.supportedLanguages=${this._supportedLanguages}
|
||||
keys="name,language"
|
||||
@value-changed=${this._valueChanged}
|
||||
?dialogInitialFocus=${!isExistingPipeline}
|
||||
?dialogInitialFocus=${!this._params.pipeline?.id}
|
||||
></assist-pipeline-detail-config>
|
||||
<assist-pipeline-detail-conversation
|
||||
.hass=${this.hass}
|
||||
@@ -231,7 +217,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
.loading=${this._submitting}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${isExistingPipeline
|
||||
${this._params.pipeline?.id
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.pipeline.detail.update_assistant_action"
|
||||
)
|
||||
@@ -277,12 +263,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
wake_word_entity: data.wake_word_entity ?? null,
|
||||
wake_word_id: data.wake_word_id ?? null,
|
||||
};
|
||||
if (
|
||||
this._params!.pipeline &&
|
||||
"id" in this._params!.pipeline &&
|
||||
!!this._params!.pipeline.id &&
|
||||
this._params!.updatePipeline
|
||||
) {
|
||||
if (this._params!.pipeline?.id) {
|
||||
await this._params!.updatePipeline(values);
|
||||
} else if (this._params!.createPipeline) {
|
||||
await this._params!.createPipeline(values);
|
||||
|
||||
@@ -6,9 +6,9 @@ import type {
|
||||
|
||||
export interface VoiceAssistantPipelineDetailsDialogParams {
|
||||
cloudActiveSubscription?: boolean;
|
||||
pipeline?: AssistPipeline | Omit<AssistPipeline, "id">;
|
||||
pipeline?: AssistPipeline;
|
||||
hideWakeWord?: boolean;
|
||||
updatePipeline?: (updates: AssistPipelineMutableParams) => Promise<unknown>;
|
||||
updatePipeline: (updates: AssistPipelineMutableParams) => Promise<unknown>;
|
||||
createPipeline?: (values: AssistPipelineMutableParams) => Promise<unknown>;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@ import { mdiDownload } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
@@ -24,10 +22,7 @@ import {
|
||||
getSummedData,
|
||||
} from "../../data/energy";
|
||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
||||
import {
|
||||
isStrategyView,
|
||||
type LovelaceViewConfig,
|
||||
} from "../../data/lovelace/config/view";
|
||||
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
|
||||
import type { StatisticValue } from "../../data/recorder";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo } from "../../types";
|
||||
@@ -38,6 +33,7 @@ import type { ExtraActionItem } from "../lovelace/hui-root";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
|
||||
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
|
||||
|
||||
@@ -52,7 +48,6 @@ const OVERVIEW_VIEW = {
|
||||
strategy: {
|
||||
type: "energy-overview",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
show_period_selector: true,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
|
||||
@@ -61,7 +56,6 @@ const ENERGY_VIEW = {
|
||||
strategy: {
|
||||
type: "energy",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
show_period_selector: true,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
|
||||
@@ -70,7 +64,6 @@ const WATER_VIEW = {
|
||||
strategy: {
|
||||
type: "water",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
show_period_selector: true,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
|
||||
@@ -79,7 +72,6 @@ const GAS_VIEW = {
|
||||
strategy: {
|
||||
type: "gas",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
show_period_selector: true,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
|
||||
@@ -182,7 +174,7 @@ class PanelEnergy extends LitElement {
|
||||
const validPaths = views.map((view) => view.path);
|
||||
const viewPath: string | undefined = this.route!.path.split("/")[1];
|
||||
if (!viewPath || !validPaths.includes(viewPath)) {
|
||||
navigate(`${this.route!.prefix}/${validPaths[0]}`, { replace: true });
|
||||
navigate(`${this.route!.prefix}/${validPaths[0]}`);
|
||||
} else {
|
||||
// Force hui-root to re-process the route by creating a new route object
|
||||
this.route = { ...this.route! };
|
||||
@@ -231,16 +223,6 @@ class PanelEnergy extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const routePath = this.route?.path?.split("/")[1] || "";
|
||||
const currentView = this._lovelace.config.views.find(
|
||||
(view) => view.path === routePath
|
||||
);
|
||||
|
||||
const showEnergySelector =
|
||||
currentView &&
|
||||
isStrategyView(currentView) &&
|
||||
currentView.strategy?.show_period_selector;
|
||||
|
||||
return html`
|
||||
<hui-root
|
||||
.hass=${this.hass}
|
||||
@@ -250,20 +232,7 @@ class PanelEnergy extends LitElement {
|
||||
.panel=${this.panel}
|
||||
.extraActionItems=${this._extraActionItems}
|
||||
@reload-energy-panel=${this._reloadConfig}
|
||||
class=${classMap({ "has-period-selector": showEnergySelector })}
|
||||
>
|
||||
</hui-root>
|
||||
${showEnergySelector
|
||||
? html`
|
||||
<ha-card class="period-selector">
|
||||
<hui-energy-period-selector
|
||||
.hass=${this.hass}
|
||||
.collectionKey=${DEFAULT_ENERGY_COLLECTION_KEY}
|
||||
vertical-opening-direction="up"
|
||||
></hui-energy-period-selector>
|
||||
</ha-card>
|
||||
`
|
||||
: nothing}
|
||||
></hui-root>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -662,6 +631,24 @@ class PanelEnergy extends LitElement {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
.toolbar {
|
||||
height: var(--header-height);
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding: 0px 12px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:host([narrow]) .toolbar {
|
||||
padding: 0 4px;
|
||||
}
|
||||
.main-title {
|
||||
margin: var(--margin-title);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
flex-grow: 1;
|
||||
}
|
||||
.centered {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -669,45 +656,6 @@ class PanelEnergy extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
hui-root.has-period-selector {
|
||||
--view-container-padding-bottom: var(--ha-space-18);
|
||||
}
|
||||
.period-selector {
|
||||
position: fixed;
|
||||
z-index: 4;
|
||||
bottom: max(var(--ha-space-4), var(--safe-area-inset-bottom, 0px));
|
||||
left: max(
|
||||
var(--mdc-drawer-width, 0px),
|
||||
var(--safe-area-inset-left, 0px)
|
||||
);
|
||||
right: var(--safe-area-inset-right, 0);
|
||||
inset-inline-start: max(
|
||||
var(--mdc-drawer-width, 0px),
|
||||
var(--safe-area-inset-left, 0px)
|
||||
);
|
||||
inset-inline-end: var(--safe-area-inset-right, 0);
|
||||
margin: 0 auto;
|
||||
max-width: calc(min(450px, 100% - var(--ha-space-4)));
|
||||
box-sizing: border-box;
|
||||
padding-left: var(--ha-space-2);
|
||||
padding-right: 0;
|
||||
padding-inline-start: var(--ha-space-4);
|
||||
padding-inline-end: 0;
|
||||
--ha-card-box-shadow:
|
||||
0px 3px 5px -1px rgba(0, 0, 0, 0.2),
|
||||
0px 6px 10px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 1px 18px 0px rgba(0, 0, 0, 0.12);
|
||||
--ha-card-border-color: var(--divider-color);
|
||||
--ha-card-border-width: var(--ha-card-border-width, 1px);
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
hui-root.has-period-selector {
|
||||
--view-container-padding-bottom: var(--ha-space-14);
|
||||
}
|
||||
.period-selector {
|
||||
bottom: max(var(--ha-space-2), var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
|
||||
|
||||
@customElement("energy-overview-view-strategy")
|
||||
@@ -63,20 +64,24 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
(source.type === "grid" && source.power?.length)
|
||||
);
|
||||
|
||||
if (hasGrid || hasBattery || hasSolar) {
|
||||
view.sections!.push({
|
||||
const overviewSection: LovelaceSectionConfig = {
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_distribution_title"
|
||||
),
|
||||
type: "energy-distribution",
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
allow_compare: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
if (hasGrid || hasBattery || hasSolar) {
|
||||
overviewSection.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_distribution_title"),
|
||||
type: "energy-distribution",
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
view.sections!.push(overviewSection);
|
||||
|
||||
if (prefs.energy_sources.length) {
|
||||
view.sections!.push({
|
||||
|
||||
@@ -53,6 +53,10 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
(d) => d.included_in_stat
|
||||
);
|
||||
|
||||
view.cards!.push({
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
view.cards!.push({
|
||||
type: "energy-compare",
|
||||
collection_key: "energy_dashboard",
|
||||
|
||||
@@ -40,6 +40,10 @@ export class GasViewStrategy extends ReactiveElement {
|
||||
|
||||
const section = view.sections![0] as LovelaceSectionConfig;
|
||||
|
||||
section.cards!.push({
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
section.cards!.push({
|
||||
type: "energy-compare",
|
||||
collection_key: collectionKey,
|
||||
|
||||
@@ -41,6 +41,10 @@ export class WaterViewStrategy extends ReactiveElement {
|
||||
|
||||
const section = view.sections![0] as LovelaceSectionConfig;
|
||||
|
||||
section.cards!.push({
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
section.cards!.push({
|
||||
type: "energy-compare",
|
||||
collection_key: collectionKey,
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../../types";
|
||||
import { showToast } from "../../util/toast";
|
||||
import "../lovelace/hui-root";
|
||||
import { expandLovelaceConfigStrategies } from "../lovelace/strategies/get-strategy";
|
||||
import { generateLovelaceDashboardStrategy } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home";
|
||||
|
||||
@@ -34,7 +34,8 @@ class PanelHome extends LitElement {
|
||||
super.willUpdate(changedProps);
|
||||
// Initial setup
|
||||
if (!this.hasUpdated) {
|
||||
this._setup();
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
this._loadConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,21 +72,6 @@ class PanelHome extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _setup() {
|
||||
try {
|
||||
const [_, data] = await Promise.all([
|
||||
this.hass.loadFragmentTranslation("lovelace"),
|
||||
fetchFrontendSystemData(this.hass.connection, "home"),
|
||||
]);
|
||||
this._config = data || {};
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to load favorites:", err);
|
||||
this._config = {};
|
||||
}
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
private _debounceRegistriesChanged = debounce(
|
||||
() => this._registriesChanged(),
|
||||
200
|
||||
@@ -111,6 +97,18 @@ class PanelHome extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
try {
|
||||
const data = await fetchFrontendSystemData(this.hass.connection, "home");
|
||||
this._config = data || {};
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to load favorites:", err);
|
||||
this._config = {};
|
||||
}
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
private async _setLovelace() {
|
||||
const strategyConfig: LovelaceDashboardStrategyConfig = {
|
||||
strategy: {
|
||||
@@ -119,7 +117,7 @@ class PanelHome extends LitElement {
|
||||
},
|
||||
};
|
||||
|
||||
const config = await expandLovelaceConfigStrategies(
|
||||
const config = await generateLovelaceDashboardStrategy(
|
||||
strategyConfig,
|
||||
this.hass
|
||||
);
|
||||
|
||||
@@ -37,7 +37,8 @@ class PanelLight extends LitElement {
|
||||
super.willUpdate(changedProps);
|
||||
// Initial setup
|
||||
if (!this.hasUpdated) {
|
||||
this._setup();
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setLovelace();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,11 +75,6 @@ class PanelLight extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _setup() {
|
||||
await this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
private _debounceRegistriesChanged = debounce(
|
||||
() => this._registriesChanged(),
|
||||
200
|
||||
|
||||
@@ -49,10 +49,6 @@ const processAreasForLight = (
|
||||
heading_style: "subtitle",
|
||||
type: "heading",
|
||||
heading: area.name,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: `/home/areas-${area.area_id}`,
|
||||
},
|
||||
});
|
||||
cards.push(...areaCards);
|
||||
}
|
||||
|
||||
@@ -295,41 +295,32 @@ export function fillDataGapsAndRoundCaps(datasets: BarSeriesOption[]) {
|
||||
});
|
||||
}
|
||||
|
||||
function getDatapointX(datapoint: NonNullable<LineSeriesOption["data"]>[0]) {
|
||||
const item =
|
||||
datapoint && typeof datapoint === "object" && "value" in datapoint
|
||||
? datapoint
|
||||
: { value: datapoint };
|
||||
return Number(item.value?.[0]);
|
||||
}
|
||||
|
||||
export function fillLineGaps(datasets: LineSeriesOption[]) {
|
||||
const buckets = Array.from(
|
||||
new Set(
|
||||
datasets
|
||||
.map((dataset) =>
|
||||
dataset.data!.map((datapoint) => getDatapointX(datapoint))
|
||||
dataset.data!.map((datapoint) => Number(datapoint![0]))
|
||||
)
|
||||
.flat()
|
||||
)
|
||||
).sort((a, b) => a - b);
|
||||
|
||||
datasets.forEach((dataset) => {
|
||||
const dataMap = new Map<number, LineDataItemOption>();
|
||||
dataset.data!.forEach((datapoint) => {
|
||||
buckets.forEach((bucket, index) => {
|
||||
for (let i = datasets.length - 1; i >= 0; i--) {
|
||||
const dataPoint = datasets[i].data![index];
|
||||
const item: LineDataItemOption =
|
||||
datapoint && typeof datapoint === "object" && "value" in datapoint
|
||||
? datapoint
|
||||
: ({ value: datapoint } as LineDataItemOption);
|
||||
const x = getDatapointX(datapoint);
|
||||
if (!Number.isNaN(x)) {
|
||||
dataMap.set(x, item);
|
||||
dataPoint && typeof dataPoint === "object" && "value" in dataPoint
|
||||
? dataPoint
|
||||
: ({ value: dataPoint } as LineDataItemOption);
|
||||
const x = item.value?.[0];
|
||||
if (x === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (Number(x) !== bucket) {
|
||||
datasets[i].data?.splice(index, 0, [bucket, 0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dataset.data = buckets.map((bucket) => dataMap.get(bucket) ?? [bucket, 0]);
|
||||
});
|
||||
|
||||
return datasets;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { EnergyData, EnergyPreferences } from "../../../../data/energy";
|
||||
import {
|
||||
formatPowerShort,
|
||||
getEnergyDataCollection,
|
||||
getPowerFromState,
|
||||
} from "../../../../data/energy";
|
||||
@@ -18,6 +17,7 @@ import type { PowerSankeyCardConfig } from "../types";
|
||||
import "../../../../components/chart/ha-sankey-chart";
|
||||
import type { Link, Node } from "../../../../components/chart/ha-sankey-chart";
|
||||
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
|
||||
import { MobileAwareMixin } from "../../../../mixins/mobile-aware-mixin";
|
||||
|
||||
@@ -26,8 +26,8 @@ const DEFAULT_CONFIG: Partial<PowerSankeyCardConfig> = {
|
||||
group_by_area: true,
|
||||
};
|
||||
|
||||
// Minimum power threshold in watts (W) to display a device node
|
||||
const MIN_POWER_THRESHOLD = 10;
|
||||
// Minimum power threshold in kW to display a device node
|
||||
const MIN_POWER_THRESHOLD = 0.01;
|
||||
|
||||
interface PowerData {
|
||||
solar: number;
|
||||
@@ -472,8 +472,8 @@ class HuiPowerSankeyCard
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatPowerShort(this.hass, value)}
|
||||
</div>`;
|
||||
${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)}
|
||||
kW</div>`;
|
||||
|
||||
/**
|
||||
* Compute real-time power data from current entity states.
|
||||
@@ -719,15 +719,14 @@ class HuiPowerSankeyCard
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current power value from entity state, normalized to watts (W)
|
||||
* Get current power value from entity state, normalized to kW
|
||||
* @param entityId - The entity ID to get power value from
|
||||
* @returns Power value in W, or 0 if entity not found or invalid
|
||||
* @returns Power value in kW, or 0 if entity not found or invalid
|
||||
*/
|
||||
private _getCurrentPower(entityId: string): number {
|
||||
// Track this entity for state change detection
|
||||
this._entities.add(entityId);
|
||||
|
||||
// getPowerFromState returns power in W
|
||||
return getPowerFromState(this.hass.states[entityId]) ?? 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -210,14 +210,9 @@ export class HuiPowerSourcesGraphCard
|
||||
const { positive, negative } = this._processData(
|
||||
statIds[key].stats.map((id: string) => {
|
||||
const stats = energyData.stats[id] ?? [];
|
||||
const currentStateWatts = getPowerFromState(this.hass.states[id]);
|
||||
if (currentStateWatts !== undefined) {
|
||||
// getPowerFromState returns power in W; convert to kW for this graph
|
||||
stats.push({
|
||||
start: now,
|
||||
end: now,
|
||||
mean: currentStateWatts / 1000,
|
||||
});
|
||||
const currentState = getPowerFromState(this.hass.states[id]);
|
||||
if (currentState !== undefined) {
|
||||
stats.push({ start: now, end: now, mean: currentState });
|
||||
}
|
||||
return stats;
|
||||
})
|
||||
|
||||
@@ -165,13 +165,15 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
<span class="value"
|
||||
>${"attribute" in this._config
|
||||
? stateObj.attributes[this._config.attribute!] !== undefined
|
||||
? html`<ha-attribute-value
|
||||
? html`
|
||||
<ha-attribute-value
|
||||
hide-unit
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.attribute=${this._config.attribute!}
|
||||
>
|
||||
</ha-attribute-value>`
|
||||
</ha-attribute-value>
|
||||
`
|
||||
: this.hass.localize("state.default.unknown")
|
||||
: (isNumericState(stateObj) || this._config.unit) &&
|
||||
stateObj.attributes.device_class !== "duration"
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { endOfDay, startOfDay } from "date-fns";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { calcDate } from "../../../common/datetime/calc_date";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import {
|
||||
findEntities,
|
||||
@@ -18,15 +15,8 @@ import "../../../components/ha-icon";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/tile/ha-tile-icon";
|
||||
import "../../../components/tile/ha-tile-info";
|
||||
import type { EnergyData } from "../../../data/energy";
|
||||
import {
|
||||
computeConsumptionData,
|
||||
formatConsumptionShort,
|
||||
getEnergyDataCollection,
|
||||
getSummedData,
|
||||
} from "../../../data/energy";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import "../../../state-display/state-display";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
@@ -45,41 +35,14 @@ const COLORS: Record<HomeSummary, string> = {
|
||||
climate: "deep-orange",
|
||||
security: "blue-grey",
|
||||
media_players: "blue",
|
||||
energy: "amber",
|
||||
};
|
||||
|
||||
@customElement("hui-home-summary-card")
|
||||
export class HuiHomeSummaryCard
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements LovelaceCard
|
||||
{
|
||||
export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: HomeSummaryCard;
|
||||
|
||||
@state() private _energyData?: EnergyData;
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
if (this._config?.summary !== "energy") {
|
||||
return [];
|
||||
}
|
||||
const collection = getEnergyDataCollection(this.hass!, {
|
||||
key: "energy_home_dashboard",
|
||||
});
|
||||
// Ensure we always show today's energy data
|
||||
collection.setPeriod(
|
||||
calcDate(new Date(), startOfDay, this.hass!.locale, this.hass!.config),
|
||||
calcDate(new Date(), endOfDay, this.hass!.locale, this.hass!.config)
|
||||
);
|
||||
return [
|
||||
collection.subscribe((data) => {
|
||||
this._energyData = data;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public setConfig(config: HomeSummaryCard): void {
|
||||
this._config = config;
|
||||
}
|
||||
@@ -251,15 +214,6 @@ export class HuiHomeSummaryCard
|
||||
})
|
||||
: this.hass.localize("ui.card.home-summary.no_media_playing");
|
||||
}
|
||||
case "energy": {
|
||||
if (!this._energyData) {
|
||||
return "";
|
||||
}
|
||||
const { summedData } = getSummedData(this._energyData);
|
||||
const { consumption } = computeConsumptionData(summedData, undefined);
|
||||
const totalConsumption = consumption.total.used_total;
|
||||
return formatConsumptionShort(this.hass, totalConsumption, "kWh");
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import {
|
||||
calcDate,
|
||||
calcDateProperty,
|
||||
@@ -70,11 +69,6 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
@property({ type: Boolean, attribute: "allow-compare" }) public allowCompare =
|
||||
true;
|
||||
|
||||
@property({ attribute: "vertical-opening-direction" })
|
||||
public verticalOpeningDirection?: "up" | "down";
|
||||
|
||||
@state() _datepickerOpen = false;
|
||||
|
||||
@state() _startDate?: Date;
|
||||
|
||||
@state() _endDate?: Date;
|
||||
@@ -156,13 +150,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
row: true,
|
||||
"datepicker-open": this._datepickerOpen,
|
||||
})}
|
||||
>
|
||||
<div class="backdrop"></div>
|
||||
<div class="row">
|
||||
<div class="label">
|
||||
${simpleRange === "day"
|
||||
? this.narrow
|
||||
@@ -214,10 +202,8 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
.ranges=${this._ranges}
|
||||
@value-changed=${this._dateRangeChanged}
|
||||
@preset-selected=${this._presetSelected}
|
||||
@toggle=${this._handleDatepickerToggle}
|
||||
minimal
|
||||
header-position
|
||||
.verticalOpeningDirection=${this.verticalOpeningDirection}
|
||||
></ha-date-range-picker>
|
||||
</div>
|
||||
|
||||
@@ -460,10 +446,6 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
this._endDate = energyData.end || endOfToday();
|
||||
}
|
||||
|
||||
private _handleDatepickerToggle(ev: CustomEvent<{ open: boolean }>) {
|
||||
this._datepickerOpen = ev.detail.open;
|
||||
}
|
||||
|
||||
private _toggleCompare(ev: CustomEvent<RequestSelectedDetail>) {
|
||||
if (ev.detail.source !== "interaction") {
|
||||
return;
|
||||
@@ -514,29 +496,6 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
flex-shrink: 0;
|
||||
--ha-button-theme-color: currentColor;
|
||||
}
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: var(--dialog-z-index, 8);
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter)
|
||||
);
|
||||
backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter)
|
||||
);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity var(--ha-animation-base-duration) ease-in-out;
|
||||
}
|
||||
.datepicker-open .backdrop {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-md-select";
|
||||
import "../../../../components/ha-md-select-option";
|
||||
import "../../../../components/ha-spinner";
|
||||
@@ -15,7 +18,6 @@ import { getDefaultPanelUrlPath } from "../../../../data/panel";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { SelectDashboardDialogParams } from "./show-select-dashboard-dialog";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
@customElement("hui-dialog-select-dashboard")
|
||||
export class HuiDialogSelectDashboard extends LitElement {
|
||||
@@ -33,29 +35,25 @@ export class HuiDialogSelectDashboard extends LitElement {
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
@state() private _open = false;
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public showDialog(params: SelectDashboardDialogParams): void {
|
||||
this._config = params.lovelaceConfig;
|
||||
this._fromUrlPath = params.urlPath;
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
this._getDashboards();
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
if (this._open) {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
this._saving = false;
|
||||
this._dashboards = undefined;
|
||||
this._toUrlPath = undefined;
|
||||
this._open = false;
|
||||
this._params = undefined;
|
||||
this._dialog?.close();
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this.closeDialog();
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -68,13 +66,23 @@ export class HuiDialogSelectDashboard extends LitElement {
|
||||
this.hass.localize("ui.panel.lovelace.editor.select_dashboard.header");
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${dialogTitle}
|
||||
.preventScrimClose=${this._saving}
|
||||
<ha-md-dialog
|
||||
open
|
||||
@closed=${this._dialogClosed}
|
||||
.ariaLabel=${dialogTitle}
|
||||
.disableCancelAction=${this._saving}
|
||||
>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._saving}
|
||||
></ha-icon-button>
|
||||
<span slot="title" .title=${dialogTitle}>${dialogTitle}</span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
${this._dashboards && !this._saving
|
||||
? html`
|
||||
<ha-md-select
|
||||
@@ -101,9 +109,9 @@ export class HuiDialogSelectDashboard extends LitElement {
|
||||
: html`<div class="loading">
|
||||
<ha-spinner size="medium"></ha-spinner>
|
||||
</div>`}
|
||||
<ha-dialog-footer slot="footer">
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._saving}
|
||||
@@ -111,7 +119,6 @@ export class HuiDialogSelectDashboard extends LitElement {
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._selectDashboard}
|
||||
.disabled=${!this._config ||
|
||||
this._fromUrlPath === this._toUrlPath ||
|
||||
@@ -119,31 +126,12 @@ export class HuiDialogSelectDashboard extends LitElement {
|
||||
>
|
||||
${this._params.actionLabel || this.hass!.localize("ui.common.move")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _getDashboards() {
|
||||
let dashboards: LovelaceDashboard[] | undefined = this._params!.dashboards;
|
||||
if (!dashboards) {
|
||||
try {
|
||||
dashboards = await fetchDashboards(this.hass);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching dashboards:", error);
|
||||
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.select_dashboard.error_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.select_dashboard.error_text"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._dashboards = [
|
||||
{
|
||||
id: "lovelace",
|
||||
@@ -153,7 +141,7 @@ export class HuiDialogSelectDashboard extends LitElement {
|
||||
title: this.hass.localize("ui.common.default"),
|
||||
mode: this.hass.panels.lovelace?.config?.mode,
|
||||
},
|
||||
...(dashboards ?? []),
|
||||
...(this._params!.dashboards || (await fetchDashboards(this.hass))),
|
||||
];
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
@@ -26,7 +26,6 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
|
||||
import { goBack, navigate } from "../../common/navigate";
|
||||
@@ -38,6 +37,7 @@ import {
|
||||
removeSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { isMobileClient } from "../../util/is_mobile";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-button-menu";
|
||||
@@ -81,7 +81,6 @@ import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { isMobileClient } from "../../util/is_mobile";
|
||||
import { showToast } from "../../util/toast";
|
||||
import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail";
|
||||
import { showNewAutomationDialog } from "../config/automation/show-dialog-new-automation";
|
||||
@@ -99,6 +98,7 @@ import "./views/hui-view";
|
||||
import type { HUIView } from "./views/hui-view";
|
||||
import "./views/hui-view-background";
|
||||
import "./views/hui-view-container";
|
||||
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
|
||||
|
||||
interface ActionItem {
|
||||
icon: string;
|
||||
@@ -1543,10 +1543,7 @@ class HUIRoot extends LitElement {
|
||||
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
padding-inline-end: var(--safe-area-inset-right);
|
||||
padding-bottom: calc(
|
||||
var(--safe-area-inset-bottom) +
|
||||
var(--view-container-padding-bottom, 0px)
|
||||
);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
}
|
||||
.narrow hui-view-container {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
|
||||
@@ -99,7 +99,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
@item-removed=${this._cardRemoved}
|
||||
invert-swap
|
||||
>
|
||||
<div class="container ${classMap({ "edit-mode": editMode })}">
|
||||
<div class="container">
|
||||
${repeat(
|
||||
cardsConfig,
|
||||
(cardConfig) => this._getKey(cardConfig),
|
||||
|
||||
@@ -9,7 +9,6 @@ export const HOME_SUMMARIES = [
|
||||
"climate",
|
||||
"security",
|
||||
"media_players",
|
||||
"energy",
|
||||
] as const;
|
||||
|
||||
export type HomeSummary = (typeof HOME_SUMMARIES)[number];
|
||||
@@ -19,7 +18,6 @@ export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
|
||||
climate: "mdi:home-thermometer",
|
||||
security: "mdi:security",
|
||||
media_players: "mdi:multimedia",
|
||||
energy: "mdi:lightning-bolt",
|
||||
};
|
||||
|
||||
export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
|
||||
@@ -27,7 +25,6 @@ export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
|
||||
climate: climateEntityFilters,
|
||||
security: securityEntityFilters,
|
||||
media_players: [{ domain: "media_player", entity_category: "none" }],
|
||||
energy: [], // Uses energy collection data
|
||||
};
|
||||
|
||||
export const getSummaryLabel = (
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "../../../../common/entity/entity_filter";
|
||||
import { clamp } from "../../../../common/number/clamp";
|
||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -26,6 +27,25 @@ export interface HomeAreaViewStrategyConfig {
|
||||
area?: string;
|
||||
}
|
||||
|
||||
const computeHeadingCard = (
|
||||
heading: string,
|
||||
icon: string,
|
||||
navigation_path?: string
|
||||
): LovelaceCardConfig =>
|
||||
({
|
||||
type: "heading",
|
||||
heading: heading,
|
||||
icon: icon,
|
||||
tap_action: navigation_path
|
||||
? {
|
||||
action: "navigate",
|
||||
navigation_path,
|
||||
}
|
||||
: {
|
||||
action: "none",
|
||||
},
|
||||
}) satisfies HeadingCardConfig;
|
||||
|
||||
@customElement("home-area-view-strategy")
|
||||
export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
@@ -94,15 +114,11 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
sections.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: getSummaryLabel(hass.localize, "light"),
|
||||
icon: HOME_SUMMARIES_ICONS.light,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/light?historyBack=1",
|
||||
},
|
||||
} satisfies HeadingCardConfig,
|
||||
computeHeadingCard(
|
||||
getSummaryLabel(hass.localize, "light"),
|
||||
HOME_SUMMARIES_ICONS.light,
|
||||
"/light?historyBack=1"
|
||||
),
|
||||
...light.map(computeTileCard),
|
||||
],
|
||||
});
|
||||
@@ -112,15 +128,11 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
sections.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: getSummaryLabel(hass.localize, "climate"),
|
||||
icon: HOME_SUMMARIES_ICONS.climate,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/climate?historyBack=1",
|
||||
},
|
||||
} satisfies HeadingCardConfig,
|
||||
computeHeadingCard(
|
||||
getSummaryLabel(hass.localize, "climate"),
|
||||
HOME_SUMMARIES_ICONS.climate,
|
||||
"/climate?historyBack=1"
|
||||
),
|
||||
...climate.map(computeTileCard),
|
||||
],
|
||||
});
|
||||
@@ -130,15 +142,11 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
sections.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: getSummaryLabel(hass.localize, "security"),
|
||||
icon: HOME_SUMMARIES_ICONS.security,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/security?historyBack=1",
|
||||
},
|
||||
} satisfies HeadingCardConfig,
|
||||
computeHeadingCard(
|
||||
getSummaryLabel(hass.localize, "security"),
|
||||
HOME_SUMMARIES_ICONS.security,
|
||||
"/security?historyBack=1"
|
||||
),
|
||||
...security.map(computeTileCard),
|
||||
],
|
||||
});
|
||||
@@ -148,15 +156,11 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
sections.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: getSummaryLabel(hass.localize, "media_players"),
|
||||
icon: HOME_SUMMARIES_ICONS.media_players,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "media-players?historyBack=1",
|
||||
},
|
||||
} satisfies HeadingCardConfig,
|
||||
computeHeadingCard(
|
||||
getSummaryLabel(hass.localize, "media_players"),
|
||||
HOME_SUMMARIES_ICONS.media_players,
|
||||
"/media-players"
|
||||
),
|
||||
...mediaPlayers.map(computeTileCard),
|
||||
],
|
||||
});
|
||||
@@ -177,17 +181,11 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
sections.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize("ui.panel.lovelace.strategy.home.scenes"),
|
||||
icon: "mdi:palette",
|
||||
tap_action: hass.user?.is_admin
|
||||
? {
|
||||
action: "navigate",
|
||||
navigation_path: "/config/scene/dashboard",
|
||||
}
|
||||
: undefined,
|
||||
} satisfies HeadingCardConfig,
|
||||
computeHeadingCard(
|
||||
hass.localize("ui.panel.lovelace.strategy.home.scenes"),
|
||||
"mdi:palette",
|
||||
hass.user?.is_admin ? "/config/scene/dashboard" : undefined
|
||||
),
|
||||
...scenes.map(computeTileCard),
|
||||
],
|
||||
});
|
||||
@@ -295,7 +293,7 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
action: "navigate",
|
||||
navigation_path: `/config/devices/device/${device.id}`,
|
||||
}
|
||||
: undefined,
|
||||
: { action: "none" },
|
||||
badges: [
|
||||
...batteryEntities.slice(0, 1).map((e) => ({
|
||||
entity: e,
|
||||
@@ -336,19 +334,11 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
sections.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.automations"
|
||||
computeHeadingCard(
|
||||
hass.localize("ui.panel.lovelace.strategy.home.automations"),
|
||||
"mdi:robot",
|
||||
hass.user?.is_admin ? "/config/automation/dashboard" : undefined
|
||||
),
|
||||
icon: "mdi:robot",
|
||||
tap_action: hass.user?.is_admin
|
||||
? {
|
||||
action: "navigate",
|
||||
navigation_path: "/config/automation/dashboard",
|
||||
}
|
||||
: undefined,
|
||||
} satisfies HeadingCardConfig,
|
||||
...automations.map(computeTileCard),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ import type {
|
||||
AreaCardConfig,
|
||||
HomeSummaryCard,
|
||||
MarkdownCardConfig,
|
||||
TileCardConfig,
|
||||
WeatherForecastCardConfig,
|
||||
} from "../../cards/types";
|
||||
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
|
||||
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||
@@ -78,11 +78,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
media_query: "(min-width: 871px)",
|
||||
};
|
||||
|
||||
const smallScreenCondition: Condition = {
|
||||
condition: "screen",
|
||||
media_query: "(max-width: 870px)",
|
||||
};
|
||||
|
||||
const floorsSections: LovelaceSectionConfig[] = [];
|
||||
for (const floorStructure of home.floors) {
|
||||
const floorId = floorStructure.id;
|
||||
@@ -141,7 +136,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
);
|
||||
const maxCommonControls = Math.max(8, favoriteEntities.length);
|
||||
|
||||
const commonControlsSectionBase = {
|
||||
const commonControlsSection = {
|
||||
strategy: {
|
||||
type: "common-controls",
|
||||
limit: maxCommonControls,
|
||||
@@ -151,20 +146,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
column_span: maxColumns,
|
||||
} as LovelaceStrategySectionConfig;
|
||||
|
||||
const commonControlsSectionMobile = {
|
||||
...commonControlsSectionBase,
|
||||
strategy: {
|
||||
...commonControlsSectionBase.strategy,
|
||||
title: hass.localize("ui.panel.lovelace.strategy.home.commonly_used"),
|
||||
},
|
||||
visibility: [smallScreenCondition],
|
||||
} as LovelaceStrategySectionConfig;
|
||||
|
||||
const commonControlsSectionDesktop = {
|
||||
...commonControlsSectionBase,
|
||||
visibility: [largeScreenCondition],
|
||||
} as LovelaceStrategySectionConfig;
|
||||
|
||||
const allEntities = Object.keys(hass.states);
|
||||
|
||||
const mediaPlayerFilter = HOME_SUMMARIES_FILTERS.media_players.map(
|
||||
@@ -189,26 +170,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
const hasClimate = findEntities(allEntities, climateFilters).length > 0;
|
||||
const hasSecurity = findEntities(allEntities, securityFilters).length > 0;
|
||||
|
||||
const weatherFilter = generateEntityFilter(hass, {
|
||||
domain: "weather",
|
||||
entity_category: "none",
|
||||
});
|
||||
|
||||
const weatherEntity = Object.keys(hass.states)
|
||||
.filter(weatherFilter)
|
||||
.sort()[0];
|
||||
|
||||
const energyPrefs = isComponentLoaded(hass, "energy")
|
||||
? // It raises if not configured, just swallow that.
|
||||
await getEnergyPreferences(hass).catch(() => undefined)
|
||||
: undefined;
|
||||
|
||||
const hasEnergy =
|
||||
energyPrefs?.energy_sources.some(
|
||||
(source) => source.type === "grid" && source.flow_from.length > 0
|
||||
) ?? false;
|
||||
|
||||
// Build summary cards (used in both mobile section and sidebar)
|
||||
const summaryCards: LovelaceCardConfig[] = [
|
||||
hasLights &&
|
||||
({
|
||||
@@ -218,6 +179,9 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
action: "navigate",
|
||||
navigation_path: "/light?historyBack=1",
|
||||
},
|
||||
grid_options: {
|
||||
columns: 12,
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
hasClimate &&
|
||||
({
|
||||
@@ -227,6 +191,9 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
action: "navigate",
|
||||
navigation_path: "/climate?historyBack=1",
|
||||
},
|
||||
grid_options: {
|
||||
columns: 12,
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
hasSecurity &&
|
||||
({
|
||||
@@ -236,6 +203,9 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
action: "navigate",
|
||||
navigation_path: "/security?historyBack=1",
|
||||
},
|
||||
grid_options: {
|
||||
columns: 12,
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
hasMediaPlayers &&
|
||||
({
|
||||
@@ -245,68 +215,76 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
action: "navigate",
|
||||
navigation_path: "media-players",
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
weatherEntity &&
|
||||
({
|
||||
type: "tile",
|
||||
entity: weatherEntity,
|
||||
name: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.summary_list.weather"
|
||||
),
|
||||
state_content: ["temperature", "state"],
|
||||
} satisfies TileCardConfig),
|
||||
hasEnergy &&
|
||||
({
|
||||
type: "home-summary",
|
||||
summary: "energy",
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/energy?historyBack=1",
|
||||
grid_options: {
|
||||
columns: 12,
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
].filter(Boolean) as LovelaceCardConfig[];
|
||||
|
||||
// Build summary cards for sidebar (full width: columns 12)
|
||||
const sidebarSummaryCards = summaryCards.map((card) => ({
|
||||
...card,
|
||||
grid_options: { columns: 12 },
|
||||
}));
|
||||
|
||||
// Build summary cards for mobile section (half width: columns 6)
|
||||
const mobileSummaryCards = summaryCards.map((card) => ({
|
||||
...card,
|
||||
grid_options: { columns: 6 },
|
||||
}));
|
||||
|
||||
// Mobile summary section (visible on small screens only)
|
||||
const mobileSummarySection: LovelaceSectionConfig | undefined =
|
||||
mobileSummaryCards.length > 0
|
||||
? {
|
||||
type: "grid",
|
||||
column_span: maxColumns,
|
||||
visibility: [smallScreenCondition],
|
||||
cards: mobileSummaryCards,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// Sidebar section
|
||||
const sidebarSection: LovelaceSectionConfig | undefined =
|
||||
sidebarSummaryCards.length > 0
|
||||
? {
|
||||
const forYouSection: LovelaceSectionConfig = {
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.for_you"
|
||||
),
|
||||
heading: hass.localize("ui.panel.lovelace.strategy.home.for_you"),
|
||||
heading_style: "title",
|
||||
visibility: [largeScreenCondition],
|
||||
},
|
||||
...sidebarSummaryCards,
|
||||
],
|
||||
};
|
||||
|
||||
const widgetSection: LovelaceSectionConfig = {
|
||||
cards: [],
|
||||
};
|
||||
|
||||
if (summaryCards.length) {
|
||||
widgetSection.cards!.push(...summaryCards);
|
||||
}
|
||||
|
||||
const weatherFilter = generateEntityFilter(hass, {
|
||||
domain: "weather",
|
||||
entity_category: "none",
|
||||
});
|
||||
|
||||
const weatherEntity = Object.keys(hass.states)
|
||||
.filter(weatherFilter)
|
||||
.sort()[0];
|
||||
|
||||
if (weatherEntity) {
|
||||
widgetSection.cards!.push({
|
||||
type: "weather-forecast",
|
||||
entity: weatherEntity,
|
||||
show_forecast: false,
|
||||
show_current: true,
|
||||
grid_options: {
|
||||
columns: 12,
|
||||
rows: "auto",
|
||||
},
|
||||
} as WeatherForecastCardConfig);
|
||||
}
|
||||
|
||||
const energyPrefs = isComponentLoaded(hass, "energy")
|
||||
? // It raises if not configured, just swallow that.
|
||||
await getEnergyPreferences(hass).catch(() => undefined)
|
||||
: undefined;
|
||||
|
||||
if (energyPrefs) {
|
||||
const grid = energyPrefs.energy_sources.find(
|
||||
(source) => source.type === "grid"
|
||||
);
|
||||
|
||||
if (grid && grid.flow_from.length > 0) {
|
||||
widgetSection.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.title_today"
|
||||
),
|
||||
type: "energy-distribution",
|
||||
collection_key: "energy_home_dashboard",
|
||||
link_dashboard: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sections = (
|
||||
[
|
||||
{
|
||||
@@ -320,9 +298,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
},
|
||||
],
|
||||
},
|
||||
mobileSummarySection,
|
||||
commonControlsSectionMobile,
|
||||
commonControlsSectionDesktop,
|
||||
commonControlsSection,
|
||||
...floorsSections,
|
||||
] satisfies (LovelaceSectionRawConfig | undefined)[]
|
||||
).filter(Boolean) as LovelaceSectionRawConfig[];
|
||||
@@ -339,16 +315,11 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
content: `## ${hass.localize("ui.panel.lovelace.strategy.home.welcome_user", { user: "{{ user }}" })}`,
|
||||
} satisfies MarkdownCardConfig,
|
||||
},
|
||||
...(sidebarSection && {
|
||||
sidebar: {
|
||||
sections: [sidebarSection],
|
||||
sections: [forYouSection, widgetSection],
|
||||
content_label: hass.localize("ui.panel.lovelace.strategy.home.home"),
|
||||
sidebar_label: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.for_you"
|
||||
),
|
||||
visibility: [largeScreenCondition],
|
||||
sidebar_label: hass.localize("ui.panel.lovelace.strategy.home.for_you"),
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,9 +61,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@state() _dragging = false;
|
||||
|
||||
@state() private _sidebarTabActive = false;
|
||||
|
||||
@state() private _sidebarVisible = true;
|
||||
@state() private _showSidebar = false;
|
||||
|
||||
private _contentScrollTop = 0;
|
||||
|
||||
@@ -125,7 +123,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
"section-visibility-changed",
|
||||
this._sectionVisibilityChanged
|
||||
);
|
||||
this._sidebarTabActive = Boolean(window.history.state?.sidebar);
|
||||
this._showSidebar = Boolean(window.history.state?.sidebar);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
@@ -146,25 +144,26 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
if (!this.lovelace) return nothing;
|
||||
|
||||
const sections = this.sections;
|
||||
const editMode = this.lovelace.editMode;
|
||||
const hasSidebar =
|
||||
this._config?.sidebar && (this._sidebarVisible || editMode);
|
||||
|
||||
const totalSectionCount =
|
||||
this._sectionColumnCount + (editMode ? 1 : 0) + (hasSidebar ? 1 : 0);
|
||||
this._sectionColumnCount +
|
||||
(this.lovelace?.editMode ? 1 : 0) +
|
||||
(this._config?.sidebar ? 1 : 0);
|
||||
const editMode = this.lovelace.editMode;
|
||||
|
||||
const maxColumnCount = this._columnsController.value ?? 1;
|
||||
|
||||
const columnCount = Math.min(maxColumnCount, totalSectionCount);
|
||||
// On mobile with sidebar, use full width for whichever view is active
|
||||
const contentColumnCount =
|
||||
hasSidebar && !this.narrow ? Math.max(1, columnCount - 1) : columnCount;
|
||||
this._config?.sidebar && !this.narrow
|
||||
? Math.max(1, columnCount - 1)
|
||||
: columnCount;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="wrapper ${classMap({
|
||||
"top-margin": Boolean(this._config?.top_margin),
|
||||
"has-sidebar": Boolean(hasSidebar),
|
||||
"has-sidebar": Boolean(this._config?.sidebar),
|
||||
narrow: this.narrow,
|
||||
})}"
|
||||
style=${styleMap({
|
||||
@@ -179,20 +178,20 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
.viewIndex=${this.index}
|
||||
.config=${this._config?.header}
|
||||
></hui-view-header>
|
||||
${this.narrow && hasSidebar
|
||||
${this.narrow && this._config?.sidebar
|
||||
? html`
|
||||
<div class="mobile-tabs">
|
||||
<ha-control-select
|
||||
.value=${this._sidebarTabActive ? "sidebar" : "content"}
|
||||
.value=${this._showSidebar ? "sidebar" : "content"}
|
||||
@value-changed=${this._viewChanged}
|
||||
.options=${[
|
||||
{
|
||||
value: "content",
|
||||
label: this._config!.sidebar!.content_label,
|
||||
label: this._config.sidebar.content_label,
|
||||
},
|
||||
{
|
||||
value: "sidebar",
|
||||
label: this._config!.sidebar!.sidebar_label,
|
||||
label: this._config.sidebar.sidebar_label,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -212,7 +211,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
<div
|
||||
class="content ${classMap({
|
||||
dense: Boolean(this._config?.dense_section_placement),
|
||||
"mobile-hidden": this.narrow && this._sidebarTabActive,
|
||||
"mobile-hidden": this.narrow && this._showSidebar,
|
||||
})}"
|
||||
>
|
||||
${repeat(
|
||||
@@ -291,16 +290,13 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
? html`
|
||||
<hui-view-sidebar
|
||||
class=${classMap({
|
||||
"mobile-hidden":
|
||||
!hasSidebar || (this.narrow && !this._sidebarTabActive),
|
||||
"mobile-hidden": this.narrow && !this._showSidebar,
|
||||
})}
|
||||
.hass=${this.hass}
|
||||
.badges=${this.badges}
|
||||
.lovelace=${this.lovelace}
|
||||
.viewIndex=${this.index}
|
||||
.config=${this._config.sidebar}
|
||||
@sidebar-visibility-changed=${this
|
||||
._handleSidebarVisibilityChanged}
|
||||
></hui-view-sidebar>
|
||||
`
|
||||
: nothing}
|
||||
@@ -418,46 +414,36 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
const newValue = ev.detail.value;
|
||||
const shouldShowSidebar = newValue === "sidebar";
|
||||
|
||||
if (shouldShowSidebar !== this._sidebarTabActive) {
|
||||
if (shouldShowSidebar !== this._showSidebar) {
|
||||
this._toggleView();
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleView() {
|
||||
// Save current scroll position
|
||||
if (this._sidebarTabActive) {
|
||||
if (this._showSidebar) {
|
||||
this._sidebarScrollTop = window.scrollY;
|
||||
} else {
|
||||
this._contentScrollTop = window.scrollY;
|
||||
}
|
||||
|
||||
this._sidebarTabActive = !this._sidebarTabActive;
|
||||
this._showSidebar = !this._showSidebar;
|
||||
|
||||
// Add sidebar state to history
|
||||
window.history.replaceState(
|
||||
{ ...window.history.state, sidebar: this._sidebarTabActive },
|
||||
{ ...window.history.state, sidebar: this._showSidebar },
|
||||
""
|
||||
);
|
||||
|
||||
// Restore scroll position after view updates
|
||||
this.updateComplete.then(() => {
|
||||
const scrollY = this._sidebarTabActive
|
||||
const scrollY = this._showSidebar
|
||||
? this._sidebarScrollTop
|
||||
: this._contentScrollTop;
|
||||
window.scrollTo(0, scrollY);
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSidebarVisibilityChanged = (
|
||||
e: CustomEvent<{ visible: boolean }>
|
||||
) => {
|
||||
this._sidebarVisible = e.detail.visible;
|
||||
// Reset sidebar tab when sidebar becomes hidden
|
||||
if (!e.detail.visible) {
|
||||
this._sidebarTabActive = false;
|
||||
}
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--row-height: var(--ha-view-sections-row-height, 56px);
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { LovelaceViewSidebarConfig } from "../../../data/lovelace/config/view";
|
||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { checkConditionsMet } from "../common/validate-condition";
|
||||
import "../sections/hui-section";
|
||||
import type { Lovelace } from "../types";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
|
||||
export const DEFAULT_VIEW_SIDEBAR_LAYOUT = "start";
|
||||
|
||||
@customElement("hui-view-sidebar")
|
||||
export class HuiViewSidebar extends ConditionalListenerMixin<LovelaceViewSidebarConfig>(
|
||||
LitElement
|
||||
) {
|
||||
export class HuiViewSidebar extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
@@ -25,38 +18,6 @@ export class HuiViewSidebar extends ConditionalListenerMixin<LovelaceViewSidebar
|
||||
|
||||
@property({ attribute: false }) public viewIndex!: number;
|
||||
|
||||
private _visible = true;
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("hass") || changedProperties.has("config")) {
|
||||
this._updateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateVisibility(conditionsMet?: boolean) {
|
||||
if (!this.hass || !this.config) return;
|
||||
|
||||
const visible =
|
||||
conditionsMet ??
|
||||
(!this.config.visibility ||
|
||||
checkConditionsMet(this.config.visibility, this.hass));
|
||||
|
||||
if (visible !== this._visible) {
|
||||
this._visible = visible;
|
||||
fireEvent(this, "sidebar-visibility-changed", { visible });
|
||||
}
|
||||
}
|
||||
|
||||
private _sectionConfigKeys = new WeakMap<LovelaceSectionConfig, string>();
|
||||
|
||||
private _getSectionKey(section: LovelaceSectionConfig) {
|
||||
if (!this._sectionConfigKeys.has(section)) {
|
||||
this._sectionConfigKeys.set(section, Math.random().toString());
|
||||
}
|
||||
return this._sectionConfigKeys.get(section)!;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.lovelace) return nothing;
|
||||
|
||||
@@ -65,8 +26,7 @@ export class HuiViewSidebar extends ConditionalListenerMixin<LovelaceViewSidebar
|
||||
return html`
|
||||
<div class="container">
|
||||
${repeat(
|
||||
this.config?.sections ?? [],
|
||||
(section) => this._getSectionKey(section),
|
||||
this.config?.sections || [],
|
||||
(section) => html`
|
||||
<hui-section
|
||||
.config=${section}
|
||||
@@ -94,7 +54,4 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-view-sidebar": HuiViewSidebar;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"sidebar-visibility-changed": { visible: boolean };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ class PanelSecurity extends LitElement {
|
||||
super.willUpdate(changedProps);
|
||||
// Initial setup
|
||||
if (!this.hasUpdated) {
|
||||
this._setup();
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setLovelace();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,11 +75,6 @@ class PanelSecurity extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _setup() {
|
||||
await this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
private _debounceRegistriesChanged = debounce(
|
||||
() => this._registriesChanged(),
|
||||
200
|
||||
|
||||
@@ -91,10 +91,6 @@ const processAreasForSecurity = (
|
||||
heading_style: "subtitle",
|
||||
type: "heading",
|
||||
heading: area.name,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: `/home/areas-${area.area_id}`,
|
||||
},
|
||||
});
|
||||
cards.push(...areaCards);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ import type {
|
||||
CustomSeriesOption,
|
||||
SankeySeriesOption,
|
||||
GraphSeriesOption,
|
||||
SunburstSeriesOption,
|
||||
} from "echarts/charts";
|
||||
import type {
|
||||
// The component option types are defined with the ComponentOption suffix
|
||||
@@ -62,7 +61,6 @@ export type ECOption = ComposeOption<
|
||||
| VisualMapComponentOption
|
||||
| SankeySeriesOption
|
||||
| GraphSeriesOption
|
||||
| SunburstSeriesOption
|
||||
>;
|
||||
|
||||
// Register the required components
|
||||
|
||||
@@ -17,7 +17,6 @@ const DEFAULT_OPTIONS: IFuseOptions<any> = {
|
||||
isCaseSensitive: false,
|
||||
threshold: 0.3,
|
||||
minMatchCharLength: 2,
|
||||
ignoreLocation: true, // don't care where the pattern is
|
||||
};
|
||||
|
||||
const DEFAULT_MIN_CHAR_LENGTH = 2;
|
||||
|
||||
@@ -48,9 +48,6 @@ export const mainStyles = css`
|
||||
*/
|
||||
--safe-width: calc(100vw - var(--safe-area-inset-left) - var(--safe-area-inset-right));
|
||||
--safe-height: calc(100vh - var(--safe-area-inset-top) - var(--safe-area-inset-bottom));
|
||||
|
||||
/* dialog backdrop filter */
|
||||
--ha-dialog-scrim-backdrop-filter: brightness(68%);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -769,9 +769,6 @@
|
||||
"no_match": "No languages found for {term}",
|
||||
"no_languages": "No languages available"
|
||||
},
|
||||
"icon-picker": {
|
||||
"no_match": "No matching icons found"
|
||||
},
|
||||
"tts-picker": {
|
||||
"tts": "Text-to-speech",
|
||||
"none": "None"
|
||||
@@ -3691,10 +3688,6 @@
|
||||
},
|
||||
"no_cloud_message": "You should have an active cloud subscription to use cloud speech services.",
|
||||
"no_cloud_action": "Subscribe"
|
||||
},
|
||||
"duplicate": {
|
||||
"error_pipeline_not_found": "Pipeline not found",
|
||||
"name": "{name} (Copy)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
@@ -4080,13 +4073,6 @@
|
||||
"services": "Services",
|
||||
"helpers": "Helpers",
|
||||
"entity_hidden": "[%key:ui::panel::config::devices::entities::hidden%]",
|
||||
"target_summary": {
|
||||
"no_target": "No target set",
|
||||
"targets": "{count} {count, plural,\n one {target}\n other {targets}\n}",
|
||||
"invalid": "Invalid target",
|
||||
"all_entities": "All entities",
|
||||
"none_entities": "No entities"
|
||||
},
|
||||
"triggers": {
|
||||
"name": "Triggers",
|
||||
"header": "When",
|
||||
@@ -4598,11 +4584,11 @@
|
||||
"service": "Perform an action",
|
||||
"target_template": "templated {name}",
|
||||
"target_every_entity": "every entity",
|
||||
"target_unknown_entity": "Unknown entity",
|
||||
"target_unknown_device": "Unknown device",
|
||||
"target_unknown_area": "Unknown area",
|
||||
"target_unknown_floor": "Unknown floor",
|
||||
"target_unknown_label": "Unknown label"
|
||||
"target_unknown_entity": "unknown entity",
|
||||
"target_unknown_device": "unknown device",
|
||||
"target_unknown_area": "unknown area",
|
||||
"target_unknown_floor": "unknown floor",
|
||||
"target_unknown_label": "unknown label"
|
||||
}
|
||||
},
|
||||
"play_media": {
|
||||
@@ -7033,7 +7019,6 @@
|
||||
"lifetime_description": "{lifetime} used",
|
||||
"lifetime_used_description": "The drive’s wear level is shown as a percentage, based on endurance indicators reported by the device via NVMe SMART or eMMC lifetime estimate fields.",
|
||||
"disk_metrics": "Disk metrics",
|
||||
"change_chart_type": "Change chart type",
|
||||
"datadisk": {
|
||||
"title": "Move data disk",
|
||||
"description": "You are currently using ''{current_path}'' as data disk. Moving the data disk will reboot your device and it's estimated to take {time} minutes. Your Home Assistant installation will not be accessible during this period. Do not disconnect the power during the move!",
|
||||
@@ -7163,9 +7148,7 @@
|
||||
},
|
||||
"home": {
|
||||
"summary_list": {
|
||||
"media_players": "Media players",
|
||||
"weather": "Weather",
|
||||
"energy": "Today's energy"
|
||||
"media_players": "Media players"
|
||||
},
|
||||
"welcome_user": "Welcome {user}",
|
||||
"summaries": "Summaries",
|
||||
@@ -7176,8 +7159,7 @@
|
||||
"scenes": "Scenes",
|
||||
"automations": "Automations",
|
||||
"for_you": "For you",
|
||||
"home": "Home",
|
||||
"commonly_used": "Commonly used"
|
||||
"home": "Home"
|
||||
},
|
||||
"common_controls": {
|
||||
"not_loaded": "Usage Prediction integration is not loaded.",
|
||||
@@ -7662,8 +7644,6 @@
|
||||
"strategy_type": "strategy"
|
||||
},
|
||||
"select_dashboard": {
|
||||
"error_title": "Error fetching dashboards",
|
||||
"error_text": "Failed to fetch dashboards, please try again.",
|
||||
"header": "Choose a dashboard",
|
||||
"cannot_move_to_strategy": "The view cannot be moved because the selected dashboard is auto-generated.",
|
||||
"get_config_failed": "Failed to load selected dashboard config.",
|
||||
|
||||
@@ -643,7 +643,7 @@ describe("computeStateDisplayFromEntityAttributes with numeric device classes",
|
||||
},
|
||||
"12"
|
||||
);
|
||||
expect(result).toBe("12m");
|
||||
expect(result).toBe("12.00 min");
|
||||
});
|
||||
it("Should format duration sensor with seconds", () => {
|
||||
const result = computeStateDisplayFromEntityAttributes(
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
computeConsumptionSingle,
|
||||
formatConsumptionShort,
|
||||
calculateSolarConsumedGauge,
|
||||
formatPowerShort,
|
||||
} from "../../src/data/energy";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
@@ -172,17 +171,6 @@ describe("Energy Short Format Test", () => {
|
||||
"151 MWh"
|
||||
);
|
||||
});
|
||||
it("Power Short Format", () => {
|
||||
assert.strictEqual(formatPowerShort(hass, 0), "0 W");
|
||||
assert.strictEqual(formatPowerShort(hass, 10), "10 W");
|
||||
assert.strictEqual(formatPowerShort(hass, 12.2), "12 W");
|
||||
assert.strictEqual(formatPowerShort(hass, 999), "999 W");
|
||||
assert.strictEqual(formatPowerShort(hass, 1000), "1 kW");
|
||||
assert.strictEqual(formatPowerShort(hass, 1234), "1.234 kW");
|
||||
assert.strictEqual(formatPowerShort(hass, 10_500), "10.5 kW");
|
||||
assert.strictEqual(formatPowerShort(hass, 1_500_000), "1.5 MW");
|
||||
assert.strictEqual(formatPowerShort(hass, -1500), "-1.5 kW");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Energy Usage Calculation Tests", () => {
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
import { assert, describe, it } from "vitest";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
|
||||
import { fillLineGaps } from "../../../../../../src/panels/lovelace/cards/energy/common/energy-chart-options";
|
||||
|
||||
// Helper to get x value from either [x,y] or {value: [x,y]} format
|
||||
function getX(item: any): number {
|
||||
return item?.value?.[0] ?? item?.[0];
|
||||
}
|
||||
|
||||
// Helper to get y value from either [x,y] or {value: [x,y]} format
|
||||
function getY(item: any): number {
|
||||
return item?.value?.[1] ?? item?.[1];
|
||||
}
|
||||
|
||||
describe("fillLineGaps", () => {
|
||||
it("fills gaps in datasets with missing timestamps", () => {
|
||||
const datasets: LineSeriesOption[] = [
|
||||
{
|
||||
type: "line",
|
||||
data: [
|
||||
[1000, 10],
|
||||
[3000, 30],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
data: [
|
||||
[1000, 100],
|
||||
[2000, 200],
|
||||
[3000, 300],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = fillLineGaps(datasets);
|
||||
|
||||
// First dataset should have gap at 2000 filled with 0
|
||||
assert.equal(result[0].data!.length, 3);
|
||||
assert.equal(getX(result[0].data![0]), 1000);
|
||||
assert.equal(getY(result[0].data![0]), 10);
|
||||
assert.equal(getX(result[0].data![1]), 2000);
|
||||
assert.equal(getY(result[0].data![1]), 0);
|
||||
assert.equal(getX(result[0].data![2]), 3000);
|
||||
assert.equal(getY(result[0].data![2]), 30);
|
||||
|
||||
// Second dataset should be unchanged
|
||||
assert.equal(result[1].data!.length, 3);
|
||||
assert.equal(getX(result[1].data![0]), 1000);
|
||||
assert.equal(getY(result[1].data![0]), 100);
|
||||
assert.equal(getX(result[1].data![1]), 2000);
|
||||
assert.equal(getY(result[1].data![1]), 200);
|
||||
assert.equal(getX(result[1].data![2]), 3000);
|
||||
assert.equal(getY(result[1].data![2]), 300);
|
||||
});
|
||||
|
||||
it("handles unsorted data from multiple sources", () => {
|
||||
// This is the bug we're fixing: when multiple power sources are combined,
|
||||
// the data may not be in chronological order
|
||||
const datasets: LineSeriesOption[] = [
|
||||
{
|
||||
type: "line",
|
||||
data: [
|
||||
[3000, 30],
|
||||
[1000, 10],
|
||||
[2000, 20],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = fillLineGaps(datasets);
|
||||
|
||||
// Data should be sorted by timestamp
|
||||
assert.equal(result[0].data!.length, 3);
|
||||
assert.equal(getX(result[0].data![0]), 1000);
|
||||
assert.equal(getY(result[0].data![0]), 10);
|
||||
assert.equal(getX(result[0].data![1]), 2000);
|
||||
assert.equal(getY(result[0].data![1]), 20);
|
||||
assert.equal(getX(result[0].data![2]), 3000);
|
||||
assert.equal(getY(result[0].data![2]), 30);
|
||||
});
|
||||
|
||||
it("handles multiple datasets with unsorted data", () => {
|
||||
const datasets: LineSeriesOption[] = [
|
||||
{
|
||||
type: "line",
|
||||
data: [
|
||||
[3000, 30],
|
||||
[1000, 10],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
data: [
|
||||
[2000, 200],
|
||||
[1000, 100],
|
||||
[3000, 300],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = fillLineGaps(datasets);
|
||||
|
||||
// First dataset should be sorted and have gap at 2000 filled
|
||||
assert.equal(result[0].data!.length, 3);
|
||||
assert.equal(getX(result[0].data![0]), 1000);
|
||||
assert.equal(getY(result[0].data![0]), 10);
|
||||
assert.equal(getX(result[0].data![1]), 2000);
|
||||
assert.equal(getY(result[0].data![1]), 0);
|
||||
assert.equal(getX(result[0].data![2]), 3000);
|
||||
assert.equal(getY(result[0].data![2]), 30);
|
||||
|
||||
// Second dataset should be sorted
|
||||
assert.equal(result[1].data!.length, 3);
|
||||
assert.equal(getX(result[1].data![0]), 1000);
|
||||
assert.equal(getY(result[1].data![0]), 100);
|
||||
assert.equal(getX(result[1].data![1]), 2000);
|
||||
assert.equal(getY(result[1].data![1]), 200);
|
||||
assert.equal(getX(result[1].data![2]), 3000);
|
||||
assert.equal(getY(result[1].data![2]), 300);
|
||||
});
|
||||
|
||||
it("handles data with object format (LineDataItemOption)", () => {
|
||||
const datasets: LineSeriesOption[] = [
|
||||
{
|
||||
type: "line",
|
||||
data: [{ value: [3000, 30] }, { value: [1000, 10] }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = fillLineGaps(datasets);
|
||||
|
||||
assert.equal(result[0].data!.length, 2);
|
||||
assert.equal(getX(result[0].data![0]), 1000);
|
||||
assert.equal(getY(result[0].data![0]), 10);
|
||||
assert.equal(getX(result[0].data![1]), 3000);
|
||||
assert.equal(getY(result[0].data![1]), 30);
|
||||
});
|
||||
|
||||
it("returns empty array for empty datasets", () => {
|
||||
const datasets: LineSeriesOption[] = [
|
||||
{
|
||||
type: "line",
|
||||
data: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = fillLineGaps(datasets);
|
||||
|
||||
assert.deepEqual(result[0].data, []);
|
||||
});
|
||||
|
||||
it("handles already sorted data with no gaps", () => {
|
||||
const datasets: LineSeriesOption[] = [
|
||||
{
|
||||
type: "line",
|
||||
data: [
|
||||
[1000, 10],
|
||||
[2000, 20],
|
||||
[3000, 30],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = fillLineGaps(datasets);
|
||||
|
||||
assert.equal(result[0].data!.length, 3);
|
||||
assert.equal(getX(result[0].data![0]), 1000);
|
||||
assert.equal(getY(result[0].data![0]), 10);
|
||||
assert.equal(getX(result[0].data![1]), 2000);
|
||||
assert.equal(getY(result[0].data![1]), 20);
|
||||
assert.equal(getX(result[0].data![2]), 3000);
|
||||
assert.equal(getY(result[0].data![2]), 30);
|
||||
});
|
||||
|
||||
it("preserves original data item properties", () => {
|
||||
const datasets: LineSeriesOption[] = [
|
||||
{
|
||||
type: "line",
|
||||
data: [
|
||||
{ value: [2000, 20], itemStyle: { color: "red" } },
|
||||
{ value: [1000, 10], itemStyle: { color: "blue" } },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = fillLineGaps(datasets);
|
||||
|
||||
// First item should be the one with timestamp 1000
|
||||
const firstItem = result[0].data![0] as any;
|
||||
assert.equal(getX(firstItem), 1000);
|
||||
assert.equal(firstItem.itemStyle.color, "blue");
|
||||
|
||||
// Second item should be the one with timestamp 2000
|
||||
const secondItem = result[0].data![1] as any;
|
||||
assert.equal(getX(secondItem), 2000);
|
||||
assert.equal(secondItem.itemStyle.color, "red");
|
||||
});
|
||||
});
|
||||
30
yarn.lock
30
yarn.lock
@@ -1282,15 +1282,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/view@npm:6.39.4, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
|
||||
version: 6.39.4
|
||||
resolution: "@codemirror/view@npm:6.39.4"
|
||||
"@codemirror/view@npm:6.39.3, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
|
||||
version: 6.39.3
|
||||
resolution: "@codemirror/view@npm:6.39.3"
|
||||
dependencies:
|
||||
"@codemirror/state": "npm:^6.5.0"
|
||||
crelt: "npm:^1.0.6"
|
||||
style-mod: "npm:^4.1.0"
|
||||
w3c-keyname: "npm:^2.2.4"
|
||||
checksum: 10/8fa1122f2c1dec38eb3a68cd7173c77ea21625bcf2164374bef482213b35222a7811bcc74f221c932c165f8d0a13a17b57456bed6fbae2b12cb9b8355664995a
|
||||
checksum: 10/a726abd34c57169ae8529d287ffcdcef14a4bf0668a56ce6da04d735ed297d0690b3c3c82e25c2ea48e262eb9ac91fbc5d813cadc97abb35cde15487040d4950
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1646,10 +1646,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/js@npm:9.39.2":
|
||||
version: 9.39.2
|
||||
resolution: "@eslint/js@npm:9.39.2"
|
||||
checksum: 10/6b7f676746f3111b5d1b23715319212ab9297868a0fa9980d483c3da8965d5841673aada2d5653e85a3f7156edee0893a7ae7035211b4efdcb2848154bb947f2
|
||||
"@eslint/js@npm:9.39.1":
|
||||
version: 9.39.1
|
||||
resolution: "@eslint/js@npm:9.39.1"
|
||||
checksum: 10/b10b9b953212c0f3ffca475159bbe519e9e98847200c7432d1637d444fddcd7b712d2b7710a7dc20510f9cfbe8db330039b2aad09cb55d9545b116d940dbeed2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8043,9 +8043,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint@npm:9.39.2":
|
||||
version: 9.39.2
|
||||
resolution: "eslint@npm:9.39.2"
|
||||
"eslint@npm:9.39.1":
|
||||
version: 9.39.1
|
||||
resolution: "eslint@npm:9.39.1"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.8.0"
|
||||
"@eslint-community/regexpp": "npm:^4.12.1"
|
||||
@@ -8053,7 +8053,7 @@ __metadata:
|
||||
"@eslint/config-helpers": "npm:^0.4.2"
|
||||
"@eslint/core": "npm:^0.17.0"
|
||||
"@eslint/eslintrc": "npm:^3.3.1"
|
||||
"@eslint/js": "npm:9.39.2"
|
||||
"@eslint/js": "npm:9.39.1"
|
||||
"@eslint/plugin-kit": "npm:^0.4.1"
|
||||
"@humanfs/node": "npm:^0.16.6"
|
||||
"@humanwhocodes/module-importer": "npm:^1.0.1"
|
||||
@@ -8088,7 +8088,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
eslint: bin/eslint.js
|
||||
checksum: 10/53ff0e9c8264e7e8d40d50fdc0c0df0b701cfc5289beedfb686c214e3e7b199702f894bbd1bb48653727bb1ecbd1147cf5f555a4ae71e1daf35020cdc9072d9f
|
||||
checksum: 10/c85fefe4a81a1a476e62087366907af830b62a6565ac153f6d50a100a42a946aeb049c3af8f06c0e091105ba0fe97ac109f379f32755a67f66ecb7d4d1e4dca3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9187,7 +9187,7 @@ __metadata:
|
||||
"@codemirror/legacy-modes": "npm:6.5.2"
|
||||
"@codemirror/search": "npm:6.5.11"
|
||||
"@codemirror/state": "npm:6.5.2"
|
||||
"@codemirror/view": "npm:6.39.4"
|
||||
"@codemirror/view": "npm:6.39.3"
|
||||
"@date-fns/tz": "npm:1.4.1"
|
||||
"@egjs/hammerjs": "npm:2.0.17"
|
||||
"@formatjs/intl-datetimeformat": "npm:6.18.2"
|
||||
@@ -9291,7 +9291,7 @@ __metadata:
|
||||
dialog-polyfill: "npm:0.5.6"
|
||||
echarts: "npm:6.0.0"
|
||||
element-internals-polyfill: "npm:3.0.2"
|
||||
eslint: "npm:9.39.2"
|
||||
eslint: "npm:9.39.1"
|
||||
eslint-config-airbnb-base: "npm:15.0.0"
|
||||
eslint-config-prettier: "npm:10.1.8"
|
||||
eslint-import-resolver-webpack: "npm:0.13.10"
|
||||
|
||||
Reference in New Issue
Block a user