mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-29 11:41:38 +00:00
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba22a12a20 | |||
| 098b54f749 | |||
| 4c6a7091a6 | |||
| 322cb35526 | |||
| c34f6bea2b | |||
| 41bf0652b0 | |||
| 23af40743b | |||
| c4326b4f3a | |||
| d248f5614f | |||
| a4da7b26ea | |||
| 3c49cdf3c0 | |||
| 26af81d1a4 | |||
| 2a08f2d79b | |||
| a5be02b743 | |||
| 4228871f00 | |||
| 9a7a8fd377 | |||
| 8b82882e15 | |||
| 2701015eda | |||
| 1991a9e493 | |||
| 2b72c54194 | |||
| a7cb2fe7a7 | |||
| 51ea0c8201 | |||
| ead7081bc6 | |||
| ee982b1899 | |||
| e8b100a39e | |||
| 50c361db62 | |||
| e7a8d15a13 | |||
| fbd0409837 | |||
| a0d100611f | |||
| a969bf1065 | |||
| a153330610 | |||
| bd2f1ca3a8 | |||
| 3263034416 | |||
| 82b28b547a | |||
| 61c2c750b4 | |||
| 117690ee70 | |||
| e753de85eb | |||
| a240019968 | |||
| 0bdf4b8777 | |||
| 6337828ed8 | |||
| b8e5af652b | |||
| e4ae29e8b5 | |||
| 08231dbbb0 | |||
| 0ca656933d | |||
| b23cf8eba4 | |||
| 61b546415d | |||
| 4e1b709303 | |||
| 34e65b302d | |||
| 336d0e1b9d | |||
| 58d4cf8d84 | |||
| d3453aff37 | |||
| 64ff2e414c | |||
| 2ca25c980f | |||
| 73d93bc601 | |||
| 5ca6a8aced | |||
| 7ff4993e0b | |||
| 4e6fbacccc | |||
| 2958d49e36 | |||
| 92289dc7ea | |||
| f6c1a890e4 | |||
| d06321ed43 | |||
| 3c3d8d9974 | |||
| 4f39fa482d | |||
| 5d0fe3236c | |||
| b86142ae50 | |||
| 5d2f3ee5e8 | |||
| e3f7c631a7 | |||
| 49f9d95853 | |||
| db3d7701b5 | |||
| 3e55acf531 | |||
| f102618d9d | |||
| a3c02b511d | |||
| 74111d248e |
@@ -89,13 +89,13 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
- name: Upload frontend build
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: frontend-build
|
||||
path: hass_frontend/
|
||||
|
||||
@@ -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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
||||
+14
-14
@@ -30,7 +30,7 @@
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.2",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/language": "6.12.2",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.5.4",
|
||||
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.2",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.3",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -83,7 +83,7 @@
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.18",
|
||||
"@swc/helpers": "0.5.19",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
@@ -92,7 +92,7 @@
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"barcode-detector": "3.0.8",
|
||||
"barcode-detector": "3.1.0",
|
||||
"color-name": "2.1.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.48.0",
|
||||
@@ -106,7 +106,7 @@
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
@@ -149,7 +149,7 @@
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.10",
|
||||
"@html-eslint/eslint-plugin": "0.56.0",
|
||||
"@html-eslint/eslint-plugin": "0.57.1",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
@@ -172,7 +172,7 @@
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.18",
|
||||
@@ -188,7 +188,7 @@
|
||||
"eslint-plugin-lit": "2.2.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"eslint-plugin-wc": "3.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"glob": "13.0.6",
|
||||
@@ -198,9 +198,9 @@
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "28.0.0",
|
||||
"jsdom": "28.1.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lint-staged": "16.3.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
@@ -210,12 +210,12 @@
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.1",
|
||||
"tar": "7.5.8",
|
||||
"tar": "7.5.9",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.54.0",
|
||||
"vite-tsconfig-paths": "6.0.5",
|
||||
"typescript-eslint": "8.56.1",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.0.18",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
@@ -235,6 +235,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.13.1"
|
||||
"node": "24.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260226.0"
|
||||
version = "20260128.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { genClientId } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
DataEntryFlowStepForm,
|
||||
} from "../data/data_entry_flow";
|
||||
import "./ha-auth-form";
|
||||
import type { HaAuthForm } from "./ha-auth-form";
|
||||
|
||||
type State = "loading" | "error" | "step";
|
||||
|
||||
@@ -52,6 +53,8 @@ export class HaAuthFlow extends LitElement {
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@query("ha-auth-form") private _form?: HaAuthForm;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
@@ -179,7 +182,7 @@ export class HaAuthFlow extends LitElement {
|
||||
<div class="action">
|
||||
<ha-button
|
||||
@click=${this._handleSubmit}
|
||||
.disabled=${this._submitting}
|
||||
.loading=${this._submitting}
|
||||
>
|
||||
${this.step.type === "form"
|
||||
? this.localize("ui.panel.page-authorize.form.next")
|
||||
@@ -370,6 +373,11 @@ export class HaAuthFlow extends LitElement {
|
||||
this._providerChanged(this.authProvider);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._form?.reportValidity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._submitting = true;
|
||||
|
||||
const postData = { ...this._stepData, client_id: this.clientId };
|
||||
|
||||
@@ -12,6 +12,10 @@ export class HaAuthFormString extends HaFormString {
|
||||
return this;
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this.querySelector("ha-auth-textfield")?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
|
||||
@@ -133,33 +133,34 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
),
|
||||
});
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
// fallback to default numeric formatting below
|
||||
}
|
||||
|
||||
const TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
};
|
||||
if (parts.length) {
|
||||
const TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
};
|
||||
|
||||
const valueParts: ValuePart[] = [];
|
||||
const valueParts: ValuePart[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
const type = TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
|
||||
if (type === "value" && last?.type === "value") {
|
||||
last.value += part.value;
|
||||
} else {
|
||||
valueParts.push({ type, value: part.value });
|
||||
for (const part of parts) {
|
||||
const type = TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
|
||||
if (type === "value" && last?.type === "value") {
|
||||
last.value += part.value;
|
||||
} else {
|
||||
valueParts.push({ type, value: part.value });
|
||||
}
|
||||
}
|
||||
return valueParts;
|
||||
}
|
||||
|
||||
return valueParts;
|
||||
}
|
||||
|
||||
// default processing of numeric values
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
import { deepActiveElement } from "../dom/deep-active-element";
|
||||
|
||||
const getClipboardFallbackRoot = (): HTMLElement => {
|
||||
const activeElement = deepActiveElement();
|
||||
if (activeElement instanceof HTMLElement) {
|
||||
let root: Node = activeElement.getRootNode();
|
||||
let host: HTMLElement | null = null;
|
||||
|
||||
while (root instanceof ShadowRoot && root.host instanceof HTMLElement) {
|
||||
host = root.host;
|
||||
root = root.host.getRootNode();
|
||||
}
|
||||
|
||||
if (host) {
|
||||
return host;
|
||||
}
|
||||
}
|
||||
|
||||
return document.body;
|
||||
};
|
||||
|
||||
export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
@@ -8,10 +29,15 @@ export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
|
||||
}
|
||||
}
|
||||
|
||||
const root = rootEl ?? document.body;
|
||||
const root = rootEl || getClipboardFallbackRoot();
|
||||
|
||||
const el = document.createElement("textarea");
|
||||
el.value = str;
|
||||
el.setAttribute("readonly", "");
|
||||
el.style.position = "fixed";
|
||||
el.style.top = "0";
|
||||
el.style.left = "0";
|
||||
el.style.opacity = "0";
|
||||
root.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
|
||||
@@ -118,8 +118,6 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public clickable = false;
|
||||
|
||||
@property({ attribute: "has-fab", type: Boolean }) public hasFab = false;
|
||||
|
||||
/**
|
||||
* Add an extra row at the bottom of the data table
|
||||
* @type {TemplateResult}
|
||||
@@ -519,7 +517,6 @@ export class HaDataTable extends LitElement {
|
||||
this._filteredData,
|
||||
localize,
|
||||
this.appendRow,
|
||||
this.hasFab,
|
||||
this.groupColumn,
|
||||
this.groupOrder,
|
||||
this._collapsedGroups,
|
||||
@@ -716,14 +713,13 @@ export class HaDataTable extends LitElement {
|
||||
data: DataTableRowData[],
|
||||
localize: LocalizeFunc,
|
||||
appendRow,
|
||||
hasFab: boolean,
|
||||
groupColumn: string | undefined,
|
||||
groupOrder: string[] | undefined,
|
||||
collapsedGroups: string[],
|
||||
sortColumn: string | undefined,
|
||||
sortDirection: SortingDirection
|
||||
) => {
|
||||
if (appendRow || hasFab || groupColumn) {
|
||||
if (appendRow || groupColumn) {
|
||||
let items = [...data];
|
||||
|
||||
if (groupColumn) {
|
||||
@@ -813,13 +809,11 @@ export class HaDataTable extends LitElement {
|
||||
items.push({ append: true, selectable: false, content: appendRow });
|
||||
}
|
||||
|
||||
if (hasFab) {
|
||||
items.push({ empty: true });
|
||||
}
|
||||
items.push({ empty: true });
|
||||
|
||||
return items;
|
||||
}
|
||||
return data;
|
||||
return [...data, { empty: true }];
|
||||
}
|
||||
);
|
||||
|
||||
@@ -871,7 +865,6 @@ export class HaDataTable extends LitElement {
|
||||
this._filteredData,
|
||||
this.localizeFunc || this.hass.localize,
|
||||
this.appendRow,
|
||||
this.hasFab,
|
||||
this.groupColumn,
|
||||
this.groupOrder,
|
||||
this._collapsedGroups,
|
||||
@@ -1089,11 +1082,8 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
.mdc-data-table__row.empty-row {
|
||||
height: max(
|
||||
var(
|
||||
--data-table-empty-row-height,
|
||||
var(--data-table-row-height, 52px)
|
||||
),
|
||||
height: var(
|
||||
--data-table-empty-row-height,
|
||||
var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -672,11 +672,11 @@ export class HaAssistChat extends LitElement {
|
||||
--markdown-code-background-color: var(--primary-background-color);
|
||||
--markdown-code-text-color: var(--primary-text-color);
|
||||
--markdown-list-indent: 1.15em;
|
||||
&:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
ha-markdown:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bouncer {
|
||||
width: 48px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-icon-button";
|
||||
@@ -133,6 +133,17 @@ export class HaBaseTimeInput extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
|
||||
|
||||
@queryAll("ha-textfield") private _inputs?: HaTextField[];
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._inputs?.every((input) => input.reportValidity()) ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.label
|
||||
|
||||
@@ -406,6 +406,18 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
transform: var(--dialog-transform);
|
||||
transition: var(--dialog-transition);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
wa-drawer {
|
||||
--wa-color-surface-raised: transparent;
|
||||
--spacing: 0;
|
||||
--size: var(--ha-bottom-sheet-height, auto);
|
||||
--show-duration: 1ms;
|
||||
--hide-duration: 1ms;
|
||||
}
|
||||
wa-drawer::part(dialog) {
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
wa-drawer::part(dialog)::backdrop {
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-bottom-sheet-scrim-backdrop-filter,
|
||||
|
||||
@@ -245,7 +245,7 @@ export class HaButton extends Button {
|
||||
}
|
||||
|
||||
.label {
|
||||
overflow: hidden;
|
||||
overflow: var(--ha-button-label-overflow, hidden);
|
||||
text-overflow: ellipsis;
|
||||
padding: var(--ha-space-1) 0;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
@property({ type: Boolean, attribute: "disable-fullscreen" })
|
||||
public disableFullscreen = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "in-dialog" })
|
||||
public inDialog = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "has-toolbar" })
|
||||
public hasToolbar = true;
|
||||
|
||||
@@ -132,6 +135,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.classList.toggle("in-dialog", this.inDialog);
|
||||
// Force update on reconnection so editor is recreated
|
||||
if (this.hasUpdated) {
|
||||
this.requestUpdate();
|
||||
@@ -150,6 +154,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
fireEvent(this, "dialog-set-fullscreen", false);
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", stopPropagation);
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
@@ -216,6 +221,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
if (changedProps.has("error")) {
|
||||
this.classList.toggle("error-state", this.error);
|
||||
}
|
||||
if (changedProps.has("inDialog")) {
|
||||
this.classList.toggle("in-dialog", this.inDialog);
|
||||
}
|
||||
if (changedProps.has("_isFullscreen")) {
|
||||
this.classList.toggle("fullscreen", this._isFullscreen);
|
||||
this._updateToolbarButtons();
|
||||
@@ -434,10 +442,19 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
private _updateFullscreenState(
|
||||
fullscreen: boolean = this._isFullscreen
|
||||
): boolean {
|
||||
const previousFullscreen = this._isFullscreen;
|
||||
|
||||
this.classList.toggle("in-dialog", this.inDialog);
|
||||
|
||||
// Update the current fullscreen state based on selected value. If fullscreen
|
||||
// is disabled, or we have no toolbar, ensure we are not in fullscreen mode.
|
||||
this._isFullscreen =
|
||||
fullscreen && !this.disableFullscreen && this.hasToolbar;
|
||||
|
||||
if (previousFullscreen !== this._isFullscreen) {
|
||||
fireEvent(this, "dialog-set-fullscreen", this._isFullscreen);
|
||||
}
|
||||
|
||||
// Return whether successfully in requested state
|
||||
return this._isFullscreen === fullscreen;
|
||||
}
|
||||
@@ -846,10 +863,10 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
:host(.fullscreen) {
|
||||
position: fixed !important;
|
||||
top: calc(var(--header-height, 56px) + 8px) !important;
|
||||
left: 8px !important;
|
||||
right: 8px !important;
|
||||
bottom: 8px !important;
|
||||
top: calc(var(--header-height, 56px) + var(--ha-space-2)) !important;
|
||||
left: var(--ha-space-2) !important;
|
||||
right: var(--ha-space-2) !important;
|
||||
bottom: var(--ha-space-2) !important;
|
||||
z-index: 6;
|
||||
border-radius: var(--ha-border-radius-lg) !important;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
|
||||
@@ -867,6 +884,17 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
:host(.in-dialog.fullscreen) {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-editor {
|
||||
padding-top: var(--code-editor-toolbar-height);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export class HaControlButton extends LitElement {
|
||||
--control-button-background-color: var(--disabled-color);
|
||||
--control-button-background-opacity: 0.2;
|
||||
--control-button-border-radius: var(--ha-border-radius-md);
|
||||
--control-button-font-weight: var(--ha-font-weight-medium);
|
||||
--control-button-padding: 8px;
|
||||
--mdc-icon-size: 20px;
|
||||
--ha-ripple-color: var(--secondary-text-color);
|
||||
@@ -59,7 +60,7 @@ export class HaControlButton extends LitElement {
|
||||
box-sizing: border-box;
|
||||
line-height: inherit;
|
||||
font-family: var(--ha-font-family-body);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
font-weight: var(--control-button-font-weight);
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
background: none;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { mdiMenuDown } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-attribute-icon";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-icon";
|
||||
@@ -16,17 +14,10 @@ export interface SelectOption {
|
||||
value: string;
|
||||
iconPath?: string;
|
||||
icon?: string;
|
||||
attributeIcon?: {
|
||||
stateObj: HassEntity;
|
||||
attribute: string;
|
||||
attributeValue?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@customElement("ha-control-select-menu")
|
||||
export class HaControlSelectMenu extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-arrow" })
|
||||
public showArrow = false;
|
||||
|
||||
@@ -47,6 +38,9 @@ export class HaControlSelectMenu extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public options: SelectOption[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
public renderIcon?: (value: string) => TemplateResult<1> | typeof nothing;
|
||||
|
||||
@query("button") private _triggerButton!: HTMLButtonElement;
|
||||
|
||||
public override render() {
|
||||
@@ -94,14 +88,8 @@ export class HaControlSelectMenu extends LitElement {
|
||||
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
|
||||
: option.icon
|
||||
? html`<ha-icon slot="icon" .icon=${option.icon}></ha-icon>`
|
||||
: option.attributeIcon
|
||||
? html`<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${option.attributeIcon.stateObj}
|
||||
.attribute=${option.attributeIcon.attribute}
|
||||
.attributeValue=${option.attributeIcon.attributeValue}
|
||||
></ha-attribute-icon>`
|
||||
: this.renderIcon
|
||||
? html`<span slot="icon">${this.renderIcon(option.value)}</span>`
|
||||
: nothing}
|
||||
${option.label}</ha-dropdown-item
|
||||
>`;
|
||||
@@ -119,24 +107,20 @@ export class HaControlSelectMenu extends LitElement {
|
||||
}
|
||||
|
||||
private _renderIcon() {
|
||||
const { iconPath, icon, attributeIcon } =
|
||||
this.getValueObject(this.options, this.value) ?? {};
|
||||
const value = this.getValueObject(this.options, this.value);
|
||||
const defaultIcon = this.querySelector("[slot='icon']");
|
||||
|
||||
return html`
|
||||
<div class="icon">
|
||||
${iconPath
|
||||
? html`<ha-svg-icon slot="icon" .path=${iconPath}></ha-svg-icon>`
|
||||
: icon
|
||||
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
|
||||
: attributeIcon
|
||||
? html`<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${attributeIcon.stateObj}
|
||||
.attribute=${attributeIcon.attribute}
|
||||
.attributeValue=${attributeIcon.attributeValue}
|
||||
></ha-attribute-icon>`
|
||||
${value?.iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${value.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: value?.icon
|
||||
? html`<ha-icon slot="icon" .icon=${value.icon}></ha-icon>`
|
||||
: this.renderIcon && this.value
|
||||
? this.renderIcon(this.value)
|
||||
: defaultIcon
|
||||
? html`<slot name="icon"></slot>`
|
||||
: nothing}
|
||||
@@ -172,12 +156,12 @@ export class HaControlSelectMenu extends LitElement {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: 1.4;
|
||||
width: auto;
|
||||
color: var(--primary-text-color);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.select-anchor {
|
||||
border: none;
|
||||
text-align: left;
|
||||
color: var(--primary-text-color);
|
||||
height: var(--control-select-menu-height);
|
||||
padding: var(--control-select-menu-padding);
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||
import { formatDateNumeric } from "../common/datetime/format_date";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -9,6 +9,7 @@ import { TimeZone } from "../data/translation";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
const loadDatePickerDialog = () => import("./ha-dialog-date-picker");
|
||||
|
||||
@@ -52,6 +53,12 @@ export class HaDateInput extends LitElement {
|
||||
|
||||
@property({ attribute: "can-clear", type: Boolean }) public canClear = false;
|
||||
|
||||
@query("ha-textfield", true) private _input?: HaTextField;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ha-textfield
|
||||
.label=${this.label}
|
||||
|
||||
+79
-24
@@ -10,7 +10,9 @@ import {
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { withViewTransition } from "../common/util/view-transition";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -127,6 +129,14 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _escapePressed = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener(
|
||||
"dialog-set-fullscreen",
|
||||
this._handleFullscreenChanged as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this.bodyContainer;
|
||||
}
|
||||
@@ -194,7 +204,10 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleShow = async () => {
|
||||
private _handleShow = async (ev: Event) => {
|
||||
if (ev.eventPhase !== Event.AT_TARGET) {
|
||||
return;
|
||||
}
|
||||
this._open = true;
|
||||
fireEvent(this, "opened");
|
||||
|
||||
@@ -220,22 +233,46 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private _handleAfterShow = () => {
|
||||
private _handleAfterShow = (ev: Event) => {
|
||||
if (ev.eventPhase !== Event.AT_TARGET) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "after-show");
|
||||
};
|
||||
|
||||
private _handleAfterHide = (ev: DialogHideEvent) => {
|
||||
if (ev.eventPhase === Event.AT_TARGET) {
|
||||
this._open = false;
|
||||
this._setFullscreen(false);
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
this.removeEventListener(
|
||||
"dialog-set-fullscreen",
|
||||
this._handleFullscreenChanged as EventListener
|
||||
);
|
||||
this._setFullscreen(false);
|
||||
super.disconnectedCallback();
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _handleFullscreenChanged(ev: HASSDomEvent<boolean>): void {
|
||||
if (!this._open) {
|
||||
this._setFullscreen(ev.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
withViewTransition(() => {
|
||||
this._setFullscreen(ev.detail);
|
||||
});
|
||||
}
|
||||
|
||||
private _setFullscreen(fullscreen: boolean): void {
|
||||
this.toggleAttribute("fullscreen", fullscreen);
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleBodyScroll(ev: Event) {
|
||||
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
|
||||
@@ -301,10 +338,27 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
|
||||
}
|
||||
|
||||
:host([width="full"]) wa-dialog {
|
||||
:host([width="full"]) wa-dialog,
|
||||
:host([fullscreen]) wa-dialog {
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
:host([fullscreen]) wa-dialog::part(dialog) {
|
||||
min-height: var(--safe-height);
|
||||
max-height: var(--safe-height);
|
||||
margin-top: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
:host([fullscreen]) .content-wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host([fullscreen]) .body {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-dialog-surface-backdrop-filter,
|
||||
@@ -356,29 +410,29 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host([type="standard"]) {
|
||||
--ha-dialog-border-radius: 0;
|
||||
}
|
||||
|
||||
wa-dialog {
|
||||
/* Make the container fill the whole screen width and not the safe width */
|
||||
--full-width: var(--ha-dialog-width-full, 100vw);
|
||||
--width: var(--full-width);
|
||||
}
|
||||
:host([type="standard"]) wa-dialog {
|
||||
/* Make the container fill the whole screen width and not the safe width */
|
||||
--full-width: var(--ha-dialog-width-full, 100vw);
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
/* Make the dialog fill the whole screen height and not the safe height */
|
||||
min-height: var(--ha-dialog-min-height, 100vh);
|
||||
min-height: var(--ha-dialog-min-height, 100dvh);
|
||||
max-height: var(--ha-dialog-max-height, 100vh);
|
||||
max-height: var(--ha-dialog-max-height, 100dvh);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
/* Use safe area as padding instead of the container size */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
/* Reset the transform to center the dialog */
|
||||
transform: none;
|
||||
}
|
||||
:host([type="standard"]) wa-dialog::part(dialog) {
|
||||
/* Make the dialog fill the whole screen height and not the safe height */
|
||||
min-height: var(--ha-dialog-min-height, 100vh);
|
||||
min-height: var(--ha-dialog-min-height, 100dvh);
|
||||
max-height: var(--ha-dialog-max-height, 100vh);
|
||||
max-height: var(--ha-dialog-max-height, 100dvh);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
/* Use safe area as padding instead of the container size */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
/* Reset the transform to center the dialog */
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,6 +519,7 @@ declare global {
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"dialog-set-fullscreen": boolean;
|
||||
opened: undefined;
|
||||
"after-show": undefined;
|
||||
closed: undefined;
|
||||
|
||||
@@ -186,9 +186,11 @@ export class HaDrawer extends DrawerBase {
|
||||
padding-inline-start var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
/* Use 1ms instead of "none" so the transitionend event still fires.
|
||||
The MDC drawer foundation relies on it to complete the close cycle. */
|
||||
.mdc-drawer,
|
||||
.mdc-drawer-app-content {
|
||||
transition: none;
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { mdiMinusThick, mdiPlusThick } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
import "./ha-button-toggle-group";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./ha-base-time-input";
|
||||
import type { HaBaseTimeInput, TimeChangedEvent } from "./ha-base-time-input";
|
||||
import "./ha-button-toggle-group";
|
||||
|
||||
export interface HaDurationData {
|
||||
days?: number;
|
||||
@@ -19,7 +19,7 @@ export interface HaDurationData {
|
||||
const FIELDS = ["milliseconds", "seconds", "minutes", "hours", "days"];
|
||||
|
||||
@customElement("ha-duration-input")
|
||||
class HaDurationInput extends LitElement {
|
||||
export class HaDurationInput extends LitElement {
|
||||
@property({ attribute: false }) public data?: HaDurationData;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -42,8 +42,19 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-base-time-input", true) private _input?: HaBaseTimeInput;
|
||||
|
||||
private _toggleNegative = false;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="row">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-form";
|
||||
import "../ha-expansion-panel";
|
||||
import "./ha-form";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
HaFormDataContainer,
|
||||
HaFormElement,
|
||||
@@ -35,6 +36,12 @@ export class HaFormExpandable extends LitElement implements HaFormElement {
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
@query("ha-form", true) private _form?: HaForm;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._form?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
private _renderDescription() {
|
||||
const description = this.computeHelper?.(this.schema);
|
||||
return description ? html`<p>${description}</p>` : nothing;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormFloatData,
|
||||
HaFormFloatSchema,
|
||||
} from "./types";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
@customElement("ha-form-float")
|
||||
export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
@@ -25,12 +25,15 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-textfield") private _input?: HaTextField;
|
||||
@query("ha-textfield", true) private _input?: HaTextField;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
}
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import "./ha-form";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-form";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
HaFormGridSchema,
|
||||
HaFormDataContainer,
|
||||
HaFormElement,
|
||||
HaFormGridSchema,
|
||||
HaFormSchema,
|
||||
} from "./types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-form-grid")
|
||||
export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
@@ -33,9 +34,22 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
this.renderRoot.querySelector("ha-form")?.focus();
|
||||
@queryAll("ha-form", true) private _forms?: HaForm[];
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const forms = this._forms ?? [];
|
||||
let valid = true;
|
||||
for (const form of forms) {
|
||||
if (!form.reportValidity()) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
|
||||
@@ -2,10 +2,11 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-slider";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-slider";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
import type {
|
||||
@@ -13,7 +14,6 @@ import type {
|
||||
HaFormIntegerData,
|
||||
HaFormIntegerSchema,
|
||||
} from "./types";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
@customElement("ha-form-integer")
|
||||
export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
@@ -29,24 +29,39 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-textfield ha-slider") private _input?:
|
||||
@query("ha-textfield, ha-slider", true) private _input?:
|
||||
| HaTextField
|
||||
| HTMLInputElement;
|
||||
|
||||
private _lastValue?: HaFormIntegerData;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const showSlider = this._showSlider();
|
||||
if (showSlider && this.schema.required && isNaN(Number(this.data))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!showSlider) {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
private _showSlider(): boolean {
|
||||
return (
|
||||
this.schema.valueMin !== undefined &&
|
||||
this.schema.valueMax !== undefined &&
|
||||
this.schema.valueMax - this.schema.valueMin < 256
|
||||
) {
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._showSlider()) {
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
|
||||
@@ -44,6 +44,13 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
this._dropdown?.focus();
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
if (!this.schema.required || (this.data && this.data.length > 0)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const options = Array.isArray(this.schema.options)
|
||||
? this.schema.options
|
||||
|
||||
@@ -8,16 +8,17 @@ import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
import "../ha-dropdown-item";
|
||||
import "../ha-svg-icon";
|
||||
import "./ha-form";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
HaFormDataContainer,
|
||||
HaFormElement,
|
||||
HaFormOptionalActionsSchema,
|
||||
HaFormSchema,
|
||||
} from "./types";
|
||||
import type { HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
|
||||
const NO_ACTIONS = [];
|
||||
|
||||
@@ -53,6 +54,11 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
this.renderRoot.querySelector("ha-form")?.focus();
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const form = this.renderRoot.querySelector<HaForm>("ha-form");
|
||||
return form ? form.reportValidity() : true;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("data")) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../ha-duration-input";
|
||||
import type { HaDurationInput } from "../ha-duration-input";
|
||||
import type { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./types";
|
||||
|
||||
@customElement("ha-form-positive_time_period_dict")
|
||||
@@ -14,12 +15,15 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-time-input", true) private _input?: HTMLElement;
|
||||
@query("ha-duration-input", true) private _input?: HaDurationInput;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
}
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { SelectSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-selector/ha-selector-select";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormSelectData,
|
||||
HaFormSelectSchema,
|
||||
} from "./types";
|
||||
import type { SelectSelector } from "../../data/selector";
|
||||
import "../ha-selector/ha-selector-select";
|
||||
|
||||
@customElement("ha-form-select")
|
||||
export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
@@ -41,6 +41,13 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
})
|
||||
);
|
||||
|
||||
public reportValidity(): boolean {
|
||||
if (!this.schema.required || this.data) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-selector-select
|
||||
|
||||
@@ -37,12 +37,15 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
|
||||
@state() protected unmaskedPassword = false;
|
||||
|
||||
@query("ha-textfield") private _input?: HaTextField;
|
||||
@query("ha-textfield", true) private _input?: HaTextField;
|
||||
|
||||
public focus(): void {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
}
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, ReactiveElement } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -24,7 +24,7 @@ const LOAD_ELEMENTS = {
|
||||
};
|
||||
|
||||
const getValue = (obj, item) =>
|
||||
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : null;
|
||||
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : undefined;
|
||||
|
||||
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||
|
||||
@@ -76,22 +76,64 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
return {};
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const root = this.renderRoot.querySelector(".root");
|
||||
if (!root) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
for (const child of root.children) {
|
||||
if (child.tagName !== "HA-ALERT") {
|
||||
if (child instanceof ReactiveElement) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await child.updateComplete;
|
||||
}
|
||||
(child as HTMLElement).focus();
|
||||
break;
|
||||
|
||||
const elements = [...root.children].filter(
|
||||
(child) => child.localName !== "ha-alert"
|
||||
) as (HTMLElement & { reportValidity?: () => boolean })[];
|
||||
|
||||
let isValid = true;
|
||||
let firstInvalidElement: HTMLElement | undefined;
|
||||
|
||||
this.schema.forEach((item, index) => {
|
||||
const element = elements[index];
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
let elementValid = true;
|
||||
|
||||
if (
|
||||
"reportValidity" in element &&
|
||||
typeof element.reportValidity === "function"
|
||||
) {
|
||||
elementValid = element.reportValidity();
|
||||
} else if (
|
||||
item.required &&
|
||||
!(
|
||||
"type" in item && ["boolean", "constant"].includes(item.type ?? "")
|
||||
) &&
|
||||
!(
|
||||
"selector" in item &&
|
||||
("boolean" in item.selector || "constant" in item.selector)
|
||||
)
|
||||
) {
|
||||
const value = getValue(this.data, item);
|
||||
elementValid = value !== undefined && value !== null && value !== "";
|
||||
}
|
||||
|
||||
if (!elementValid) {
|
||||
isValid = false;
|
||||
if (!firstInvalidElement) {
|
||||
firstInvalidElement = element;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (firstInvalidElement) {
|
||||
firstInvalidElement.focus?.();
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
@@ -105,11 +147,6 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
}
|
||||
}
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="root" part="root">
|
||||
|
||||
@@ -38,7 +38,7 @@ export class HaBadge extends LitElement {
|
||||
font-weight: var(--ha-heading-badge-font-weight, 400);
|
||||
line-height: var(--ha-heading-badge-line-height, 20px);
|
||||
letter-spacing: 0.1px;
|
||||
--mdc-icon-size: 14px;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
::slotted([slot="icon"]) {
|
||||
--ha-icon-display: block;
|
||||
|
||||
@@ -74,6 +74,7 @@ export class HaIconButton extends LitElement {
|
||||
);
|
||||
--wa-color-on-normal: currentColor;
|
||||
--wa-color-fill-quiet: transparent;
|
||||
--ha-button-label-overflow: visible;
|
||||
}
|
||||
ha-button::after {
|
||||
content: "";
|
||||
|
||||
@@ -84,13 +84,11 @@ export class HaMarkdown extends LitElement {
|
||||
ha-markdown-element > :is(ol, ul) {
|
||||
padding-inline-start: var(--markdown-list-indent, revert);
|
||||
}
|
||||
li {
|
||||
&:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
& > input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
li:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
}
|
||||
li:has(input[type="checkbox"]) > input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
svg {
|
||||
background-color: var(--markdown-svg-background-color, none);
|
||||
@@ -137,10 +135,10 @@ export class HaMarkdown extends LitElement {
|
||||
--markdown-table-border-width: 0;
|
||||
--markdown-table-padding-inline: 0;
|
||||
--markdown-table-padding-block: 0;
|
||||
th,
|
||||
td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
table[role="presentation"] th,
|
||||
table[role="presentation"] td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
table[role="presentation"] td[valign="top"],
|
||||
table[role="presentation"] th[valign="top"] {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { DateSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-date-input";
|
||||
import type { HaDateInput } from "../ha-date-input";
|
||||
|
||||
@customElement("ha-selector-date")
|
||||
export class HaDateSelector extends LitElement {
|
||||
@@ -20,6 +21,12 @@ export class HaDateSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@query("ha-date-input", true) private _input?: HaDateInput;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-date-input
|
||||
|
||||
@@ -29,6 +29,10 @@ export class HaDateTimeSelector extends LitElement {
|
||||
|
||||
@query("ha-time-input") private _timeInput!: HaTimeInput;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._dateInput.reportValidity() && this._timeInput.reportValidity();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const values =
|
||||
typeof this.value === "string" ? this.value.split(" ") : undefined;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { DurationSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HaDurationData } from "../ha-duration-input";
|
||||
import "../ha-duration-input";
|
||||
import type { HaDurationData, HaDurationInput } from "../ha-duration-input";
|
||||
|
||||
@customElement("ha-selector-duration")
|
||||
export class HaTimeDuration extends LitElement {
|
||||
@@ -25,6 +25,12 @@ export class HaTimeDuration extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@query("ha-duration-input", true) private _input?: HaDurationInput;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
private _data = memoizeOne(
|
||||
(value?: HaDurationData | string | number): HaDurationData | undefined => {
|
||||
if (typeof value === "number") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { NumberSelector } from "../../data/selector";
|
||||
@@ -8,6 +8,7 @@ import type { HomeAssistant } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-slider";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
|
||||
@customElement("ha-selector-number")
|
||||
export class HaNumberSelector extends LitElement {
|
||||
@@ -30,8 +31,14 @@ export class HaNumberSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-textfield", true) private _input?: HaTextField | HTMLInputElement;
|
||||
|
||||
private _valueStr = "";
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (changedProps.has("value")) {
|
||||
if (this._valueStr === "" || this.value !== Number(this._valueStr)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { StringSelector } from "../../data/selector";
|
||||
@@ -32,11 +32,18 @@ export class HaTextSelector extends LitElement {
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
|
||||
@query("ha-textfield, ha-textarea") private _input?: HTMLInputElement;
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
(
|
||||
this.renderRoot.querySelector("ha-textarea, ha-textfield") as HTMLElement
|
||||
)?.focus();
|
||||
this._input?.focus();
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
if (this.selector.text?.multiple) {
|
||||
return true;
|
||||
}
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { TimeSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-time-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
|
||||
@customElement("ha-selector-time")
|
||||
export class HaTimeSelector extends LitElement {
|
||||
@@ -20,6 +21,12 @@ export class HaTimeSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@query("ha-time-input") private _input?: HaTimeInput;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-time-input
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import type { Selector } from "../../data/selector";
|
||||
@@ -94,9 +94,27 @@ export class HaSelector extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public context?: Record<string, any>;
|
||||
|
||||
@query("#selector", true) private _selectorElement?: HTMLElement;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
if (
|
||||
this._selectorElement &&
|
||||
"reportValidity" in this._selectorElement &&
|
||||
typeof this._selectorElement.reportValidity === "function"
|
||||
) {
|
||||
return this._selectorElement?.reportValidity() ?? true;
|
||||
}
|
||||
if (this.required) {
|
||||
return (
|
||||
this.value !== undefined && this.value !== null && this.value !== ""
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
(this.renderRoot.querySelector("#selector") as HTMLElement)?.focus();
|
||||
this._selectorElement?.focus();
|
||||
}
|
||||
|
||||
private get _type() {
|
||||
|
||||
@@ -37,8 +37,8 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
import type { UpdateEntity } from "../data/update";
|
||||
import { updateCanInstall } from "../data/update";
|
||||
import { showEditSidebarDialog } from "../dialogs/sidebar/show-dialog-edit-sidebar";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
@@ -981,7 +981,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
ha-md-list-item,
|
||||
ha-md-list-item .item-text,
|
||||
.title {
|
||||
transition: none;
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { useAmPm } from "../common/datetime/use_am_pm";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./ha-base-time-input";
|
||||
import type { HaBaseTimeInput, TimeChangedEvent } from "./ha-base-time-input";
|
||||
|
||||
@customElement("ha-time-input")
|
||||
export class HaTimeInput extends LitElement {
|
||||
@@ -26,6 +26,12 @@ export class HaTimeInput extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
|
||||
|
||||
@query("ha-base-time-input") private _input?: HaBaseTimeInput;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const useAMPM = useAmPm(this.locale);
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-top-app-bar {
|
||||
transition: none;
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
|
||||
@@ -298,7 +298,7 @@ export class TopAppBarBaseBase extends BaseElement {
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-top-app-bar {
|
||||
transition: none;
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
|
||||
|
||||
@@ -47,6 +47,9 @@ export class HaYamlEditor extends LitElement {
|
||||
@property({ type: Boolean, attribute: "disable-fullscreen" })
|
||||
public disableFullscreen = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "in-dialog" })
|
||||
public inDialog = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ attribute: "copy-clipboard", type: Boolean })
|
||||
@@ -101,6 +104,13 @@ export class HaYamlEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public disableCodeEditorFullscreen(): void {
|
||||
this.disableFullscreen = true;
|
||||
if (this._codeEditor) {
|
||||
this._codeEditor.disableFullscreen = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._yaml === undefined) {
|
||||
return nothing;
|
||||
@@ -114,6 +124,7 @@ export class HaYamlEditor extends LitElement {
|
||||
.value=${this._yaml}
|
||||
.readOnly=${this.readOnly}
|
||||
.disableFullscreen=${this.disableFullscreen}
|
||||
.inDialog=${this.inDialog}
|
||||
mode="yaml"
|
||||
autocomplete-entities
|
||||
autocomplete-icons
|
||||
|
||||
@@ -37,9 +37,11 @@ export interface LovelaceViewHeaderConfig {
|
||||
badges_wrap?: "wrap" | "scroll";
|
||||
}
|
||||
|
||||
export const DEFAULT_FOOTER_MAX_WIDTH_PX = 600;
|
||||
|
||||
export interface LovelaceViewFooterConfig {
|
||||
card?: LovelaceCardConfig;
|
||||
column_span?: number;
|
||||
max_width?: number;
|
||||
}
|
||||
|
||||
export interface LovelaceViewSidebarConfig {
|
||||
|
||||
@@ -7,10 +7,23 @@ export type SystemLogLevel =
|
||||
| "info"
|
||||
| "debug";
|
||||
|
||||
export type SystemLogErrorType =
|
||||
| "auth"
|
||||
| "connection"
|
||||
| "invalid_response"
|
||||
| "rate_limit"
|
||||
| "server"
|
||||
| "slow_setup"
|
||||
| "timeout"
|
||||
| "ssl"
|
||||
| "statistics"
|
||||
| "dns";
|
||||
|
||||
export interface LoggedError {
|
||||
name: string;
|
||||
message: [string];
|
||||
level: SystemLogLevel;
|
||||
error_type?: SystemLogErrorType;
|
||||
source: [string, number];
|
||||
exception: string;
|
||||
count: number;
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-attribute-icon";
|
||||
import "../../../components/ha-control-select-menu";
|
||||
import "../../../components/ha-icon-button-group";
|
||||
import "../../../components/ha-icon-button-toggle";
|
||||
@@ -39,6 +40,38 @@ class MoreInfoClimate extends LitElement {
|
||||
|
||||
@state() private _mainControl: MainControl = "temperature";
|
||||
|
||||
private _renderPresetModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="preset_mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
private _renderFanModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="fan_mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
private _renderSwingModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="swing_mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
private _renderSwingHorizontalModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="swing_horizontal_mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (
|
||||
changedProps.has("stateObj") &&
|
||||
@@ -205,12 +238,8 @@ class MoreInfoClimate extends LitElement {
|
||||
"preset_mode",
|
||||
mode
|
||||
),
|
||||
attributeIcon: {
|
||||
stateObj,
|
||||
attribute: "preset_mode",
|
||||
attributeValue: mode,
|
||||
},
|
||||
}))}
|
||||
.renderIcon=${this._renderPresetModeIcon}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
@@ -234,12 +263,8 @@ class MoreInfoClimate extends LitElement {
|
||||
"fan_mode",
|
||||
mode
|
||||
),
|
||||
attributeIcon: {
|
||||
stateObj,
|
||||
attribute: "fan_mode",
|
||||
attributeValue: mode,
|
||||
},
|
||||
}))}
|
||||
.renderIcon=${this._renderFanModeIcon}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
@@ -263,12 +288,8 @@ class MoreInfoClimate extends LitElement {
|
||||
"swing_mode",
|
||||
mode
|
||||
),
|
||||
attributeIcon: {
|
||||
stateObj,
|
||||
attribute: "swing_mode",
|
||||
attributeValue: mode,
|
||||
},
|
||||
}))}
|
||||
.renderIcon=${this._renderSwingModeIcon}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
@@ -297,13 +318,9 @@ class MoreInfoClimate extends LitElement {
|
||||
"swing_horizontal_mode",
|
||||
mode
|
||||
),
|
||||
attributeIcon: {
|
||||
stateObj,
|
||||
attribute: "swing_horizontal_mode",
|
||||
attributeValue: mode,
|
||||
},
|
||||
})
|
||||
)}
|
||||
.renderIcon=${this._renderSwingHorizontalModeIcon}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
|
||||
@@ -40,6 +40,22 @@ class MoreInfoFan extends LitElement {
|
||||
|
||||
@state() public _presetMode?: string;
|
||||
|
||||
private _renderPresetModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="preset_mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
private _renderDirectionIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="direction"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
private _toggle = () => {
|
||||
const service = this.stateObj?.state === "on" ? "turn_off" : "turn_on";
|
||||
forwardHaptic(this, "light");
|
||||
@@ -192,15 +208,9 @@ class MoreInfoFan extends LitElement {
|
||||
"preset_mode",
|
||||
mode
|
||||
),
|
||||
attributeIcon: this.stateObj
|
||||
? {
|
||||
stateObj: this.stateObj,
|
||||
attribute: "preset_mode",
|
||||
attributeValue: mode,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
)}
|
||||
.renderIcon=${this._renderPresetModeIcon}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
@@ -226,14 +236,8 @@ class MoreInfoFan extends LitElement {
|
||||
direction
|
||||
)
|
||||
: direction,
|
||||
attributeIcon: this.stateObj
|
||||
? {
|
||||
stateObj: this.stateObj,
|
||||
attribute: "direction",
|
||||
attributeValue: direction,
|
||||
}
|
||||
: undefined,
|
||||
}))}
|
||||
.renderIcon=${this._renderDirectionIcon}
|
||||
>
|
||||
<ha-attribute-icon
|
||||
slot="icon"
|
||||
|
||||
@@ -23,6 +23,14 @@ class MoreInfoHumidifier extends LitElement {
|
||||
|
||||
@state() public _mode?: string;
|
||||
|
||||
private _renderModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("stateObj")) {
|
||||
@@ -106,14 +114,8 @@ class MoreInfoHumidifier extends LitElement {
|
||||
mode
|
||||
)
|
||||
: mode,
|
||||
attributeIcon: stateObj
|
||||
? {
|
||||
stateObj,
|
||||
attribute: "mode",
|
||||
attributeValue: mode,
|
||||
}
|
||||
: undefined,
|
||||
})) || []}
|
||||
.renderIcon=${this._renderModeIcon}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
|
||||
@@ -55,6 +55,14 @@ class MoreInfoLight extends LitElement {
|
||||
|
||||
@state() private _mainControl: MainControl = "brightness";
|
||||
|
||||
private _renderEffectIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="effect"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
protected updated(changedProps: PropertyValues<typeof this>): void {
|
||||
if (changedProps.has("stateObj")) {
|
||||
this._effect = this.stateObj?.attributes.effect;
|
||||
@@ -271,15 +279,9 @@ class MoreInfoLight extends LitElement {
|
||||
effect
|
||||
)
|
||||
: effect,
|
||||
attributeIcon: this.stateObj
|
||||
? {
|
||||
stateObj: this.stateObj,
|
||||
attribute: "effect",
|
||||
attributeValue: effect,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
)}
|
||||
.renderIcon=${this._renderEffectIcon}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiCreation}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
|
||||
@@ -24,6 +24,14 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj?: WaterHeaterEntity;
|
||||
|
||||
private _renderOperationModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="operation_mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
@@ -85,12 +93,8 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
.map((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityState(stateObj, mode),
|
||||
attributeIcon: {
|
||||
stateObj,
|
||||
attribute: "operation_mode",
|
||||
attributeValue: mode,
|
||||
},
|
||||
}))}
|
||||
.renderIcon=${this._renderOperationModeIcon}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
|
||||
@@ -2,17 +2,27 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
|
||||
import checkValidDate from "../../common/datetime/check_valid_date";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import "../../components/ha-attribute-value";
|
||||
import "../../components/ha-card";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import { computeShownAttributes } from "../../data/entity/entity_attributes";
|
||||
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../../components/ha-yaml-editor";
|
||||
|
||||
interface DetailsViewParams {
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
interface DetailEntry {
|
||||
translationKey: LocalizeKeys;
|
||||
value: string;
|
||||
}
|
||||
|
||||
@customElement("ha-more-info-details")
|
||||
class HaMoreInfoDetails extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -21,6 +31,8 @@ class HaMoreInfoDetails extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public params?: DetailsViewParams;
|
||||
|
||||
@property({ attribute: false }) public yamlMode = false;
|
||||
|
||||
@state() private _stateObj?: HassEntity;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
@@ -37,60 +49,128 @@ class HaMoreInfoDetails extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const translatedState = this.hass.formatEntityState(this._stateObj);
|
||||
const detailsAttributes = computeShownAttributes(this._stateObj);
|
||||
const detailsAttributeSet = new Set(detailsAttributes);
|
||||
const builtInAttributes = Object.keys(this._stateObj.attributes).filter(
|
||||
(attribute) => !detailsAttributeSet.has(attribute)
|
||||
const { stateEntries, attributes, yamlData } = this._getDetailData(
|
||||
this._stateObj
|
||||
);
|
||||
const allAttributes = [...detailsAttributes, ...builtInAttributes];
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<section class="section">
|
||||
<h2 class="section-title">
|
||||
${this.hass.localize(
|
||||
"ui.components.entity.entity-state-picker.state"
|
||||
)}
|
||||
</h2>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="attribute-group">
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.translated"
|
||||
)}
|
||||
${this.yamlMode
|
||||
? html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.value=${yamlData}
|
||||
read-only
|
||||
auto-update
|
||||
in-dialog
|
||||
></ha-yaml-editor>`
|
||||
: html`
|
||||
<section class="section">
|
||||
<h2 class="section-title">
|
||||
${this.hass.localize(
|
||||
"ui.components.entity.entity-state-picker.state"
|
||||
)}
|
||||
</h2>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="data-group">
|
||||
${stateEntries.map(
|
||||
(entry) =>
|
||||
html`<div class="data-entry">
|
||||
<div class="key">
|
||||
${this.hass.localize(entry.translationKey)}
|
||||
</div>
|
||||
<div class="value">${entry.value}</div>
|
||||
</div>`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="value">${translatedState}</div>
|
||||
</div>
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.raw")}
|
||||
</div>
|
||||
<div class="value">${this._stateObj.state}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</section>
|
||||
</ha-card>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2 class="section-title">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.attributes")}
|
||||
</h2>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="attribute-group">
|
||||
${this._renderAttributes(allAttributes)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</section>
|
||||
<section class="section">
|
||||
<h2 class="section-title">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.attributes"
|
||||
)}
|
||||
</h2>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="data-group">
|
||||
${this._renderAttributes(attributes)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</section>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getDetailData = memoizeOne(
|
||||
(
|
||||
stateObj: HassEntity
|
||||
): {
|
||||
stateEntries: DetailEntry[];
|
||||
attributes: string[];
|
||||
yamlData: {
|
||||
state: {
|
||||
translated: string;
|
||||
raw: string;
|
||||
last_changed: string;
|
||||
last_updated: string;
|
||||
};
|
||||
attributes: Record<string, string>;
|
||||
};
|
||||
} => {
|
||||
const translatedState = this.hass.formatEntityState(stateObj);
|
||||
|
||||
const detailsAttributes = computeShownAttributes(stateObj);
|
||||
const detailsAttributeSet = new Set(detailsAttributes);
|
||||
const builtInAttributes = Object.keys(stateObj.attributes).filter(
|
||||
(attribute) => !detailsAttributeSet.has(attribute)
|
||||
);
|
||||
|
||||
return {
|
||||
stateEntries: [
|
||||
{
|
||||
translationKey: "ui.dialogs.more_info_control.translated",
|
||||
value: translatedState,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.dialogs.more_info_control.raw",
|
||||
value: stateObj.state,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.dialogs.more_info_control.last_changed",
|
||||
value: this._formatTimestamp(stateObj.last_changed),
|
||||
},
|
||||
{
|
||||
translationKey: "ui.dialogs.more_info_control.last_updated",
|
||||
value: this._formatTimestamp(stateObj.last_updated),
|
||||
},
|
||||
],
|
||||
attributes: [...detailsAttributes, ...builtInAttributes],
|
||||
yamlData: {
|
||||
state: {
|
||||
translated: translatedState,
|
||||
raw: stateObj.state,
|
||||
last_changed: stateObj.last_changed,
|
||||
last_updated: stateObj.last_updated,
|
||||
},
|
||||
attributes: stateObj.attributes,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
private _formatTimestamp(value: string): string {
|
||||
const date = new Date(value);
|
||||
|
||||
return checkValidDate(date)
|
||||
? formatDateTimeWithSeconds(date, this.hass.locale, this.hass.config)
|
||||
: value;
|
||||
}
|
||||
|
||||
private _renderAttributes(attributes: string[]) {
|
||||
if (attributes.length === 0) {
|
||||
return html`<div class="empty">
|
||||
@@ -159,7 +239,7 @@ class HaMoreInfoDetails extends LitElement {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.attribute-group .data-entry:last-of-type {
|
||||
.data-group .data-entry:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
mdiChartBoxOutline,
|
||||
mdiClose,
|
||||
mdiCodeBraces,
|
||||
mdiCogOutline,
|
||||
mdiDevices,
|
||||
mdiDotsVertical,
|
||||
@@ -118,6 +119,8 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@query(".content") private _contentElement?: HTMLDivElement;
|
||||
|
||||
@query("ha-adaptive-dialog") private _dialogElement?: HTMLElement;
|
||||
|
||||
@state() private _entityId?: string | null;
|
||||
|
||||
@state() private _data?: Record<string, any>;
|
||||
@@ -132,6 +135,8 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@state() private _infoEditMode = false;
|
||||
|
||||
@state() private _detailsYamlMode = false;
|
||||
|
||||
@state() private _isEscapeEnabled = true;
|
||||
|
||||
@state() private _sensorNumericDeviceClasses?: string[] = [];
|
||||
@@ -174,6 +179,10 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
const dialog = this._dialogElement?.shadowRoot?.querySelector("ha-dialog");
|
||||
if (dialog) {
|
||||
fireEvent(dialog as HTMLElement, "dialog-set-fullscreen", false);
|
||||
}
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
@@ -182,6 +191,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
this._parentEntityIds = [];
|
||||
this._entry = undefined;
|
||||
this._infoEditMode = false;
|
||||
this._detailsYamlMode = false;
|
||||
this._initialView = DEFAULT_VIEW;
|
||||
this._currView = DEFAULT_VIEW;
|
||||
this._childView = undefined;
|
||||
@@ -250,7 +260,13 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _goBack() {
|
||||
if (this._childView) {
|
||||
const dialog =
|
||||
this._dialogElement?.shadowRoot?.querySelector("ha-dialog");
|
||||
if (dialog) {
|
||||
fireEvent(dialog as HTMLElement, "dialog-set-fullscreen", false);
|
||||
}
|
||||
this._childView = undefined;
|
||||
this._detailsYamlMode = false;
|
||||
return;
|
||||
}
|
||||
if (this._initialView !== this._currView) {
|
||||
@@ -314,6 +330,14 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
this._infoEditMode = !this._infoEditMode;
|
||||
}
|
||||
|
||||
private _toggleDetailsYamlMode() {
|
||||
const dialog = this._dialogElement?.shadowRoot?.querySelector("ha-dialog");
|
||||
if (dialog) {
|
||||
fireEvent(dialog as HTMLElement, "dialog-set-fullscreen", false);
|
||||
}
|
||||
this._detailsYamlMode = !this._detailsYamlMode;
|
||||
}
|
||||
|
||||
private _handleToggleInfoEditModeEvent(ev) {
|
||||
this._infoEditMode = ev.detail;
|
||||
}
|
||||
@@ -637,7 +661,18 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing}
|
||||
: this._childView?.viewTag === "ha-more-info-details"
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="headerActionItems"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.toggle_yaml_mode"
|
||||
)}
|
||||
.path=${mdiCodeBraces}
|
||||
@click=${this._toggleDetailsYamlMode}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class=${classMap({
|
||||
"content-wrapper": true,
|
||||
@@ -663,6 +698,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._childView.viewParams,
|
||||
yamlMode: this._detailsYamlMode,
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
@@ -728,9 +764,25 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
const previousChildView = changedProps.get("_childView") as
|
||||
| ChildView
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
previousChildView?.viewTag === "ha-more-info-details" &&
|
||||
this._childView?.viewTag !== "ha-more-info-details"
|
||||
) {
|
||||
const dialog =
|
||||
this._dialogElement?.shadowRoot?.querySelector("ha-dialog");
|
||||
if (dialog) {
|
||||
fireEvent(dialog as HTMLElement, "dialog-set-fullscreen", false);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("_currView")) {
|
||||
this._childView = undefined;
|
||||
this._infoEditMode = false;
|
||||
this._detailsYamlMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,11 @@ import {
|
||||
mdiUnfoldLessHorizontal,
|
||||
mdiUnfoldMoreHorizontal,
|
||||
} from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/chips/ha-assist-chip";
|
||||
@@ -83,7 +84,15 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
* Do we need to add padding for a fab.
|
||||
* @type {Boolean}
|
||||
*/
|
||||
@property({ attribute: "has-fab", type: Boolean }) public hasFab = false;
|
||||
@property({ attribute: "has-fab", type: Boolean, reflect: true })
|
||||
public hasFab = false;
|
||||
|
||||
/**
|
||||
* Show tabs on top or at bottom (narrow) of the page.
|
||||
* @type {Boolean}
|
||||
*/
|
||||
@property({ attribute: "show-tabs", type: Boolean, reflect: true })
|
||||
public showTabs = false;
|
||||
|
||||
/**
|
||||
* Add an extra row at the bottom of the data table
|
||||
@@ -200,7 +209,19 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
protected willUpdate() {
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (
|
||||
changedProperties.has("tabs") ||
|
||||
(changedProperties.has("hass") &&
|
||||
(this.hass?.config.components !==
|
||||
changedProperties.get("hass")?.config.components ||
|
||||
this.hass?.userData?.showAdvanced !==
|
||||
changedProperties.get("hass")?.userData?.showAdvanced))
|
||||
) {
|
||||
this.showTabs =
|
||||
this.tabs.filter((page) => canShowPage(this.hass, page)).length > 1;
|
||||
}
|
||||
|
||||
if (this.hasUpdated) {
|
||||
return;
|
||||
}
|
||||
@@ -491,7 +512,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
.noDataText=${this.noDataText}
|
||||
.filter=${this.filter}
|
||||
.selectable=${this._selectMode}
|
||||
.hasFab=${this.hasFab}
|
||||
.id=${this.id}
|
||||
.clickable=${this.clickable}
|
||||
.appendRow=${this.appendRow}
|
||||
@@ -713,6 +733,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
--data-table-border-width: 0;
|
||||
--data-table-empty-row-height: var(--safe-area-inset-bottom, 0px);
|
||||
}
|
||||
:host(:not([narrow])) ha-data-table,
|
||||
.pane {
|
||||
@@ -725,6 +746,23 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
);
|
||||
display: block;
|
||||
}
|
||||
/* Last content row should keep the same padding above the fab as the fab
|
||||
has to the bottom (16px standard fab bottom padding) + the safe-area inset. */
|
||||
:host([has-fab]) ha-data-table {
|
||||
--data-table-empty-row-height: calc(
|
||||
48px + 16px * 2 + var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
}
|
||||
/* In narrow view with tabs shown at the bottom, the tab bar already
|
||||
accounts for safe-area-inset-bottom. No extra empty-row height is needed. */
|
||||
:host([narrow][show-tabs]:not([has-fab])) ha-data-table {
|
||||
--data-table-empty-row-height: 0px;
|
||||
}
|
||||
/* Reserve space for fab + doubled narrow-mode bottom padding (28px * 2)
|
||||
when using narrow layout with bottom tabs. */
|
||||
:host([narrow][show-tabs][has-fab]) ha-data-table {
|
||||
--data-table-empty-row-height: calc(48px + 28px * 2);
|
||||
}
|
||||
|
||||
.pane-content {
|
||||
height: calc(
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface PageNavigation {
|
||||
}
|
||||
|
||||
@customElement("hass-tabs-subpage")
|
||||
class HassTabsSubpage extends LitElement {
|
||||
export class HassTabsSubpage extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localizeFunc?: LocalizeFunc;
|
||||
@@ -65,6 +65,14 @@ class HassTabsSubpage extends LitElement {
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "has-fab" }) public hasFab = false;
|
||||
|
||||
/**
|
||||
* Whether tabs are shown (2 or more tabs visible).
|
||||
* When both, show-tabs and narrow are true, tabs are shown as bottom bar.
|
||||
* @type {Boolean}
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "show-tabs", reflect: true })
|
||||
public showTabs = false;
|
||||
|
||||
@state() private _activeTab?: PageNavigation;
|
||||
|
||||
// @ts-ignore
|
||||
@@ -83,6 +91,7 @@ class HassTabsSubpage extends LitElement {
|
||||
const shownTabs = tabs.filter((page) => canShowPage(this.hass, page));
|
||||
|
||||
if (shownTabs.length < 2) {
|
||||
this.showTabs = false;
|
||||
if (shownTabs.length === 1) {
|
||||
const page = shownTabs[0];
|
||||
return [
|
||||
@@ -92,6 +101,7 @@ class HassTabsSubpage extends LitElement {
|
||||
return [""];
|
||||
}
|
||||
|
||||
this.showTabs = true;
|
||||
return shownTabs.map(
|
||||
(page) => html`
|
||||
<a href=${page.path} @click=${this._tabClicked}>
|
||||
@@ -135,7 +145,6 @@ class HassTabsSubpage extends LitElement {
|
||||
this.narrow,
|
||||
this.localizeFunc || this.hass.localize
|
||||
);
|
||||
const showTabs = tabs.length > 1;
|
||||
return html`
|
||||
<div class="toolbar ${classMap({ narrow: this.narrow })}">
|
||||
<slot name="toolbar">
|
||||
@@ -160,12 +169,12 @@ class HassTabsSubpage extends LitElement {
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`}
|
||||
${this.narrow || !showTabs
|
||||
${this.narrow || !this.showTabs
|
||||
? html`<div class="main-title">
|
||||
<slot name="header">${!showTabs ? tabs[0] : ""}</slot>
|
||||
<slot name="header">${!this.showTabs ? tabs[0] : ""}</slot>
|
||||
</div>`
|
||||
: ""}
|
||||
${showTabs && !this.narrow
|
||||
${this.showTabs && !this.narrow
|
||||
? html`<div id="tabbar">${tabs}</div>`
|
||||
: ""}
|
||||
<div id="toolbar-icon">
|
||||
@@ -173,13 +182,11 @@ class HassTabsSubpage extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
${showTabs && this.narrow
|
||||
${this.showTabs && this.narrow
|
||||
? html`<div id="tabbar" class="bottom-bar">${tabs}</div>`
|
||||
: ""}
|
||||
</div>
|
||||
<div
|
||||
class=${classMap({ container: true, tabs: showTabs && this.narrow })}
|
||||
>
|
||||
<div class="container">
|
||||
${this.pane
|
||||
? html`<div class="pane">
|
||||
<div class="shadow-container"></div>
|
||||
@@ -188,15 +195,12 @@ class HassTabsSubpage extends LitElement {
|
||||
</div>
|
||||
</div>`
|
||||
: nothing}
|
||||
<div
|
||||
class="content ha-scrollbar ${classMap({ tabs: showTabs })}"
|
||||
@scroll=${this._saveScrollPos}
|
||||
>
|
||||
<div class="content ha-scrollbar" @scroll=${this._saveScrollPos}>
|
||||
<slot></slot>
|
||||
${this.hasFab ? html`<div class="fab-bottom-space"></div>` : nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div id="fab" class=${classMap({ tabs: showTabs })}>
|
||||
<div id="fab">
|
||||
<slot name="fab"></slot>
|
||||
</div>
|
||||
`;
|
||||
@@ -373,7 +377,7 @@ class HassTabsSubpage extends LitElement {
|
||||
margin-left: var(--safe-area-inset-left);
|
||||
margin-inline-start: var(--safe-area-inset-left);
|
||||
}
|
||||
:host([narrow]) .content.tabs {
|
||||
:host([narrow][show-tabs]) .content {
|
||||
/* Bottom bar reuses header height */
|
||||
margin-bottom: calc(
|
||||
var(--header-height, 0px) + var(--safe-area-inset-bottom, 0px)
|
||||
@@ -384,7 +388,7 @@ class HassTabsSubpage extends LitElement {
|
||||
height: calc(64px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
:host([narrow]) .content.tabs .fab-bottom-space {
|
||||
:host([narrow][show-tabs]) .content .fab-bottom-space {
|
||||
height: calc(80px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
@@ -400,7 +404,7 @@ class HassTabsSubpage extends LitElement {
|
||||
justify-content: flex-end;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
:host([narrow]) #fab.tabs {
|
||||
:host([narrow][show-tabs]) #fab {
|
||||
bottom: calc(84px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
#fab[is-wide] {
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { LitElement, PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { computeLocalize } from "../common/translations/localize";
|
||||
import { computeDirectionStyles } from "../common/util/compute_rtl";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import type { Constructor, Resources } from "../types";
|
||||
import { getLocalLanguage, getTranslation } from "../util/common-translation";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import { computeDirectionStyles } from "../common/util/compute_rtl";
|
||||
|
||||
const empty = () => "";
|
||||
|
||||
@@ -28,16 +28,16 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
||||
this._initializeLocalizeLite();
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
computeDirectionStyles(
|
||||
translationMetadata.translations[this.language!].isRTL,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (!this.updated || changedProperties.has("language")) {
|
||||
computeDirectionStyles(
|
||||
translationMetadata.translations[this.language!].isRTL,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
if (changedProperties.get("language")) {
|
||||
this._resources = undefined;
|
||||
this._initializeLocalizeLite();
|
||||
|
||||
@@ -35,7 +35,7 @@ import type { RepositoryDialogParams } from "./show-dialog-repositories";
|
||||
class AppsRepositoriesDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@query("#repository_input", true) private _optionInput?: HaTextField;
|
||||
@query("#repository_input") private _optionInput?: HaTextField;
|
||||
|
||||
@state() private _repositories?: HassioAddonRepository[];
|
||||
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isTemplate } from "../../../../../common/string/has-template";
|
||||
import type { WaitAction } from "../../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../../components/ha-form/types";
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "wait_template",
|
||||
selector: {
|
||||
template: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "timeout",
|
||||
required: false,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "continue_on_timeout",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const;
|
||||
type TimeoutType = "string_template" | "object_template" | "duration";
|
||||
|
||||
@customElement("ha-automation-action-wait_template")
|
||||
export class HaWaitAction extends LitElement implements ActionElement {
|
||||
@@ -38,12 +25,47 @@ export class HaWaitAction extends LitElement implements ActionElement {
|
||||
return { wait_template: "", continue_on_timeout: true };
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(timeoutType: TimeoutType) =>
|
||||
[
|
||||
{
|
||||
name: "wait_template",
|
||||
selector: { template: {} },
|
||||
},
|
||||
{
|
||||
name: "timeout",
|
||||
required: false,
|
||||
selector:
|
||||
timeoutType === "string_template"
|
||||
? { template: {} }
|
||||
: timeoutType === "object_template"
|
||||
? { object: {} }
|
||||
: { duration: { enable_millisecond: true } },
|
||||
},
|
||||
{
|
||||
name: "continue_on_timeout",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const timeout = this.action.timeout;
|
||||
const timeoutType: TimeoutType =
|
||||
typeof timeout === "string" && isTemplate(timeout)
|
||||
? "string_template"
|
||||
: typeof timeout === "object" &&
|
||||
timeout !== null &&
|
||||
Object.values(timeout).some(
|
||||
(v) => typeof v === "string" && isTemplate(v)
|
||||
)
|
||||
? "object_template"
|
||||
: "duration";
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.action}
|
||||
.schema=${SCHEMA}
|
||||
.schema=${this._schema(timeoutType)}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
></ha-form>
|
||||
@@ -51,7 +73,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<typeof SCHEMA>
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.type.wait_template.${
|
||||
|
||||
@@ -27,19 +27,15 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { promiseTimeout } from "../../../common/util/promise-timeout";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-fade-in";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import type {
|
||||
@@ -72,27 +68,22 @@ import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type {
|
||||
Entries,
|
||||
HomeAssistant,
|
||||
Route,
|
||||
ValueChangedEvent,
|
||||
} from "../../../types";
|
||||
import type { Entries, ValueChangedEvent } from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
|
||||
import {
|
||||
type EntityRegistryUpdate,
|
||||
showAutomationSaveDialog,
|
||||
} from "./automation-save-dialog/show-dialog-automation-save";
|
||||
import { showAutomationSaveDialog } from "./automation-save-dialog/show-dialog-automation-save";
|
||||
import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialog/show-dialog-automation-save-timeout";
|
||||
import "./blueprint-automation-editor";
|
||||
import {
|
||||
AutomationScriptEditorMixin,
|
||||
automationScriptEditorStyles,
|
||||
} from "./ha-automation-script-editor-mixin";
|
||||
import "./manual-automation-editor";
|
||||
import type { HaManualAutomationEditor } from "./manual-automation-editor";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
@@ -119,53 +110,13 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement("ha-automation-editor")
|
||||
export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
KeyboardShortcutMixin(LitElement)
|
||||
export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationConfig>(
|
||||
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public automationId: string | null = null;
|
||||
|
||||
@property({ attribute: false }) public entityId: string | null = null;
|
||||
|
||||
@property({ attribute: false }) public automations!: AutomationEntity[];
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _config?: AutomationConfig;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _errors?: string;
|
||||
|
||||
@state() private _yamlErrors?: string;
|
||||
|
||||
@state() private _entityId?: string;
|
||||
|
||||
@state() private _mode: "gui" | "yaml" = "gui";
|
||||
|
||||
@state() private _readOnly = false;
|
||||
|
||||
@state() private _validationErrors?: (string | TemplateResult)[];
|
||||
|
||||
@state() private _blueprintConfig?: BlueprintAutomationConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
|
||||
transformer: function (this: HaAutomationEditor, value) {
|
||||
return value.find(({ entity_id }) => entity_id === this._entityId);
|
||||
},
|
||||
watch: ["_entityId"],
|
||||
})
|
||||
private _registryEntry?: EntityRegistryEntry;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityRegistry!: EntityRegistryEntry[];
|
||||
@@ -180,24 +131,18 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
|
||||
private _configSubscriptionsId = 1;
|
||||
|
||||
private _entityRegistryUpdate?: EntityRegistryUpdate;
|
||||
|
||||
private _newAutomationId?: string;
|
||||
|
||||
private _entityRegCreated?: (
|
||||
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
|
||||
) => void;
|
||||
|
||||
private _undoRedoController = new UndoRedoController<AutomationConfig>(this, {
|
||||
apply: (config) => this._applyUndoRedo(config),
|
||||
currentConfig: () => this._config!,
|
||||
currentConfig: () => this.config!,
|
||||
});
|
||||
|
||||
protected willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (
|
||||
this._entityRegCreated &&
|
||||
this.entityRegCreated &&
|
||||
this._newAutomationId &&
|
||||
changedProps.has("_entityRegistry")
|
||||
) {
|
||||
@@ -207,26 +152,22 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
entity.unique_id === this._newAutomationId
|
||||
);
|
||||
if (automation) {
|
||||
this._entityRegCreated(automation);
|
||||
this._entityRegCreated = undefined;
|
||||
this.entityRegCreated(automation);
|
||||
this.entityRegCreated = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._config) {
|
||||
return html`
|
||||
<ha-fade-in .delay=${500}>
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-fade-in>
|
||||
`;
|
||||
if (!this.config) {
|
||||
return this.renderLoading();
|
||||
}
|
||||
|
||||
const stateObj = this._entityId
|
||||
? this.hass.states[this._entityId]
|
||||
const stateObj = this.currentEntityId
|
||||
? this.hass.states[this.currentEntityId]
|
||||
: undefined;
|
||||
|
||||
const useBlueprint = "use_blueprint" in this._config;
|
||||
const useBlueprint = "use_blueprint" in this.config;
|
||||
const shortcutIcon = isMac
|
||||
? html`<ha-svg-icon .path=${mdiAppleKeyboardCommand}></ha-svg-icon>`
|
||||
: this.hass.localize("ui.panel.config.automation.editor.ctrl");
|
||||
@@ -236,11 +177,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.backCallback=${this._backTapped}
|
||||
.header=${this._config.alias ||
|
||||
.backCallback=${this.backTapped}
|
||||
.header=${this.config.alias ||
|
||||
this.hass.localize("ui.panel.config.automation.editor.default_name")}
|
||||
>
|
||||
${this._mode === "gui" && !this.narrow
|
||||
${this.mode === "gui" && !this.narrow
|
||||
? html`<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.label=${this.hass.localize("ui.common.undo")}
|
||||
@@ -284,7 +225,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
</span>
|
||||
</ha-tooltip>`
|
||||
: nothing}
|
||||
${this._config?.id && !this.narrow
|
||||
${this.config?.id && !this.narrow
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@@ -308,7 +249,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${this._mode === "gui" && this.narrow
|
||||
${this.mode === "gui" && this.narrow
|
||||
? html`<ha-dropdown-item
|
||||
value="undo"
|
||||
.disabled=${!this._undoRedoController.canUndo}
|
||||
@@ -342,7 +283,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
|
||||
<ha-dropdown-item .disabled=${!stateObj} value="category">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.scene.picker.${this._registryEntry?.categories?.automation ? "edit_category" : "assign_category"}`
|
||||
`ui.panel.config.scene.picker.${this.registryEntry?.categories?.automation ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
@@ -366,9 +307,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
|
||||
<ha-dropdown-item
|
||||
value="rename"
|
||||
.disabled=${this._readOnly ||
|
||||
.disabled=${this.readOnly ||
|
||||
!this.automationId ||
|
||||
this._mode === "yaml"}
|
||||
this.mode === "yaml"}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.rename")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
@@ -377,7 +318,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
@click=${this._promptAutomationMode}
|
||||
.disabled=${this._readOnly || this._mode === "yaml"}
|
||||
.disabled=${this.readOnly || this.mode === "yaml"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.change_mode"
|
||||
@@ -391,12 +332,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
: nothing}
|
||||
|
||||
<ha-dropdown-item
|
||||
.disabled=${!!this._blueprintConfig ||
|
||||
(!this._readOnly && !this.automationId)}
|
||||
.disabled=${!!this.blueprintConfig ||
|
||||
(!this.readOnly && !this.automationId)}
|
||||
value="duplicate"
|
||||
>
|
||||
${this.hass.localize(
|
||||
this._readOnly
|
||||
this.readOnly
|
||||
? "ui.panel.config.automation.editor.migrate"
|
||||
: "ui.panel.config.automation.editor.duplicate"
|
||||
)}
|
||||
@@ -410,7 +351,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
value="take_control"
|
||||
.disabled=${this._readOnly}
|
||||
.disabled=${this.readOnly}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.take_control"
|
||||
@@ -422,7 +363,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
|
||||
<ha-dropdown-item value="toggle_yaml_mode">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${this._mode === "gui" ? "yaml" : "ui"}`
|
||||
`ui.panel.config.automation.editor.edit_${this.mode === "gui" ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
@@ -456,10 +397,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<div
|
||||
class=${this._mode === "yaml" ? "yaml-mode" : ""}
|
||||
class=${this.mode === "yaml" ? "yaml-mode" : ""}
|
||||
@subscribe-automation-config=${this._subscribeAutomationConfig}
|
||||
>
|
||||
${this._mode === "gui"
|
||||
${this.mode === "gui"
|
||||
? html`
|
||||
<div>
|
||||
${useBlueprint
|
||||
@@ -469,10 +410,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.stateObj=${stateObj}
|
||||
.config=${this._config}
|
||||
.disabled=${this._readOnly}
|
||||
.saving=${this._saving}
|
||||
.dirty=${this._dirty}
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.saving=${this.saving}
|
||||
.dirty=${this.dirty}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
></blueprint-automation-editor>
|
||||
@@ -483,16 +424,16 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.stateObj=${stateObj}
|
||||
.config=${this._config}
|
||||
.disabled=${this._readOnly}
|
||||
.dirty=${this._dirty}
|
||||
.saving=${this._saving}
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.dirty=${this.dirty}
|
||||
.saving=${this.saving}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
@editor-save=${this._handleSaveAutomation}
|
||||
>
|
||||
<div class="alert-wrapper" slot="alerts">
|
||||
${this._errors || stateObj?.state === UNAVAILABLE
|
||||
${this.errors || stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
.title=${stateObj?.state === UNAVAILABLE
|
||||
@@ -501,7 +442,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
)
|
||||
: undefined}
|
||||
>
|
||||
${this._errors || this._validationErrors}
|
||||
${this.errors || this.validationErrors}
|
||||
${stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-svg-icon
|
||||
slot="icon"
|
||||
@@ -510,7 +451,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${this._blueprintConfig
|
||||
${this.blueprintConfig
|
||||
? html`<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.confirm_take_control"
|
||||
@@ -518,21 +459,21 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
<div slot="action" style="display: flex;">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._takeControlSave}
|
||||
@click=${this.takeControlSave}
|
||||
>${this.hass.localize(
|
||||
"ui.common.yes"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._revertBlueprint}
|
||||
@click=${this.revertBlueprint}
|
||||
>${this.hass.localize(
|
||||
"ui.common.no"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-alert>`
|
||||
: this._readOnly
|
||||
: this.readOnly
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
dismissable
|
||||
@@ -575,7 +516,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: this._mode === "yaml"
|
||||
: this.mode === "yaml"
|
||||
? html`${stateObj?.state === "off"
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
@@ -598,7 +539,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
.readOnly=${this._readOnly}
|
||||
.readOnly=${this.readOnly}
|
||||
@value-changed=${this._yamlChanged}
|
||||
@editor-save=${this._handleSaveAutomation}
|
||||
.showErrors=${false}
|
||||
@@ -606,9 +547,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
></ha-yaml-editor>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${this._dirty ? "dirty" : ""}
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
.label=${this.hass.localize("ui.common.save")}
|
||||
.disabled=${this._saving}
|
||||
.disabled=${this.saving}
|
||||
extended
|
||||
@click=${this._handleSaveAutomation}
|
||||
>
|
||||
@@ -645,7 +586,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
this.hass
|
||||
) {
|
||||
const initData = getAutomationEditorInitData();
|
||||
this._dirty = !!initData;
|
||||
this.dirty = !!initData;
|
||||
let baseConfig: Partial<AutomationConfig> = { description: "" };
|
||||
if (!initData || !("use_blueprint" in initData)) {
|
||||
baseConfig = {
|
||||
@@ -656,35 +597,35 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
actions: [],
|
||||
};
|
||||
}
|
||||
this._config = {
|
||||
this.config = {
|
||||
...baseConfig,
|
||||
...(initData ? normalizeAutomationConfig(initData) : initData),
|
||||
} as AutomationConfig;
|
||||
this._entityId = undefined;
|
||||
this._readOnly = false;
|
||||
this.currentEntityId = undefined;
|
||||
this.readOnly = false;
|
||||
}
|
||||
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
getAutomationStateConfig(this.hass, this.entityId).then((c) => {
|
||||
this._config = normalizeAutomationConfig(c.config);
|
||||
this.config = normalizeAutomationConfig(c.config);
|
||||
this._checkValidation();
|
||||
});
|
||||
this._entityId = this.entityId;
|
||||
this._dirty = false;
|
||||
this._readOnly = true;
|
||||
this.currentEntityId = this.entityId;
|
||||
this.dirty = false;
|
||||
this.readOnly = true;
|
||||
}
|
||||
|
||||
if (
|
||||
changedProps.has("automations") &&
|
||||
this.automationId &&
|
||||
!this._entityId
|
||||
!this.currentEntityId
|
||||
) {
|
||||
this._setEntityId();
|
||||
}
|
||||
|
||||
if (changedProps.has("_config")) {
|
||||
if (changedProps.has("config")) {
|
||||
Object.values(this._configSubscriptions).forEach((sub) =>
|
||||
sub(this._config)
|
||||
sub(this.config)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -693,24 +634,24 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
const automation = this.automations.find(
|
||||
(entity: AutomationEntity) => entity.attributes.id === this.automationId
|
||||
);
|
||||
this._entityId = automation?.entity_id;
|
||||
this.currentEntityId = automation?.entity_id;
|
||||
}
|
||||
|
||||
private async _checkValidation() {
|
||||
this._validationErrors = undefined;
|
||||
if (!this._entityId || !this._config) {
|
||||
this.validationErrors = undefined;
|
||||
if (!this.currentEntityId || !this.config) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass.states[this._entityId];
|
||||
const stateObj = this.hass.states[this.currentEntityId];
|
||||
if (stateObj?.state !== UNAVAILABLE) {
|
||||
return;
|
||||
}
|
||||
const validation = await validateConfig(this.hass, {
|
||||
triggers: this._config.triggers,
|
||||
conditions: this._config.conditions,
|
||||
actions: this._config.actions,
|
||||
triggers: this.config.triggers,
|
||||
conditions: this.config.conditions,
|
||||
actions: this.config.actions,
|
||||
});
|
||||
this._validationErrors = (
|
||||
this.validationErrors = (
|
||||
Object.entries(validation) as Entries<typeof validation>
|
||||
).map(([key, value]) =>
|
||||
value.valid
|
||||
@@ -728,9 +669,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
this.hass,
|
||||
this.automationId as string
|
||||
);
|
||||
this._dirty = false;
|
||||
this._readOnly = false;
|
||||
this._config = normalizeAutomationConfig(config);
|
||||
this.dirty = false;
|
||||
this.readOnly = false;
|
||||
this.config = normalizeAutomationConfig(config);
|
||||
this._checkValidation();
|
||||
} catch (err: any) {
|
||||
const entity = this._entityRegistry.find(
|
||||
@@ -761,34 +702,27 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
private _valueChanged(ev: ValueChangedEvent<AutomationConfig>) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this._config) {
|
||||
this._undoRedoController.commit(this._config);
|
||||
if (this.config) {
|
||||
this._undoRedoController.commit(this.config);
|
||||
}
|
||||
|
||||
this._config = ev.detail.value;
|
||||
if (this._readOnly) {
|
||||
this.config = ev.detail.value;
|
||||
if (this.readOnly) {
|
||||
return;
|
||||
}
|
||||
this._dirty = true;
|
||||
this._errors = undefined;
|
||||
this.dirty = true;
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
private _showInfo() {
|
||||
if (!this.hass || !this._entityId) {
|
||||
if (!this.hass || !this.currentEntityId) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "hass-more-info", { entityId: this._entityId });
|
||||
}
|
||||
|
||||
private _showSettings() {
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: this._entityId!,
|
||||
view: "settings",
|
||||
});
|
||||
fireEvent(this, "hass-more-info", { entityId: this.currentEntityId });
|
||||
}
|
||||
|
||||
private _editCategory() {
|
||||
if (!this._registryEntry) {
|
||||
if (!this.registryEntry) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.scene.picker.no_category_support"
|
||||
@@ -801,36 +735,36 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
showAssignCategoryDialog(this, {
|
||||
scope: "automation",
|
||||
entityReg: this._registryEntry,
|
||||
entityReg: this.registryEntry,
|
||||
});
|
||||
}
|
||||
|
||||
private async _showTrace() {
|
||||
if (this._config?.id) {
|
||||
const result = await this._confirmUnsavedChanged();
|
||||
if (this.config?.id) {
|
||||
const result = await this.confirmUnsavedChanged();
|
||||
if (result) {
|
||||
navigate(
|
||||
`/config/automation/trace/${encodeURIComponent(this._config.id)}`
|
||||
`/config/automation/trace/${encodeURIComponent(this.config.id)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _runActions() {
|
||||
if (!this.hass || !this._entityId) {
|
||||
if (!this.hass || !this.currentEntityId) {
|
||||
return;
|
||||
}
|
||||
triggerAutomationActions(
|
||||
this.hass,
|
||||
this.hass.states[this._entityId].entity_id
|
||||
this.hass.states[this.currentEntityId].entity_id
|
||||
);
|
||||
}
|
||||
|
||||
private async _toggle(): Promise<void> {
|
||||
if (!this.hass || !this._entityId) {
|
||||
if (!this.hass || !this.currentEntityId) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass.states[this._entityId];
|
||||
const stateObj = this.hass.states[this.currentEntityId];
|
||||
const service = stateObj.state === "off" ? "turn_on" : "turn_off";
|
||||
await this.hass.callService("automation", service, {
|
||||
entity_id: stateObj.entity_id,
|
||||
@@ -838,42 +772,42 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
|
||||
private _preprocessYaml() {
|
||||
if (!this._config) {
|
||||
if (!this.config) {
|
||||
return {};
|
||||
}
|
||||
const cleanConfig: AutomationConfig = { ...this._config };
|
||||
const cleanConfig: AutomationConfig = { ...this.config };
|
||||
delete cleanConfig.id;
|
||||
return cleanConfig;
|
||||
}
|
||||
|
||||
private _yamlChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._dirty = true;
|
||||
this.dirty = true;
|
||||
if (!ev.detail.isValid) {
|
||||
this._yamlErrors = ev.detail.errorMsg;
|
||||
this.yamlErrors = ev.detail.errorMsg;
|
||||
return;
|
||||
}
|
||||
this._yamlErrors = undefined;
|
||||
this._config = {
|
||||
id: this._config?.id,
|
||||
this.yamlErrors = undefined;
|
||||
this.config = {
|
||||
id: this.config?.id,
|
||||
...normalizeAutomationConfig(ev.detail.value),
|
||||
};
|
||||
this._errors = undefined;
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
private async _confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (!this._dirty) {
|
||||
protected async confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (!this.dirty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
showAutomationSaveDialog(this, {
|
||||
config: this._config!,
|
||||
config: this.config!,
|
||||
domain: "automation",
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this._config = config;
|
||||
this._entityRegistryUpdate = entityRegistryUpdate;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this.requestUpdate();
|
||||
|
||||
const id = this.automationId || String(Date.now());
|
||||
@@ -889,8 +823,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
},
|
||||
onClose: () => resolve(false),
|
||||
onDiscard: () => resolve(true),
|
||||
entityRegistryUpdate: this._entityRegistryUpdate,
|
||||
entityRegistryEntry: this._registryEntry,
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
entityRegistryEntry: this.registryEntry,
|
||||
title: this.hass.localize(
|
||||
this.automationId
|
||||
? "ui.panel.config.automation.editor.leave.unsaved_confirm_title"
|
||||
@@ -906,15 +840,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
});
|
||||
}
|
||||
|
||||
private _backTapped = async () => {
|
||||
const result = await this._confirmUnsavedChanged();
|
||||
if (result) {
|
||||
afterNextRender(() => goBack("/config"));
|
||||
}
|
||||
};
|
||||
|
||||
private async _takeControl() {
|
||||
const config = this._config as BlueprintAutomationConfig;
|
||||
const config = this.config as BlueprintAutomationConfig;
|
||||
|
||||
try {
|
||||
const result = await substituteBlueprint(
|
||||
@@ -931,35 +858,20 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
description: config.description,
|
||||
};
|
||||
|
||||
this._blueprintConfig = config;
|
||||
this._config = newConfig;
|
||||
if (this._mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
|
||||
this.blueprintConfig = config;
|
||||
this.config = newConfig;
|
||||
if (this.mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
|
||||
}
|
||||
this._readOnly = true;
|
||||
this._errors = undefined;
|
||||
this.readOnly = true;
|
||||
this.errors = undefined;
|
||||
} catch (err: any) {
|
||||
this._errors = err.message;
|
||||
this.errors = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
private _revertBlueprint() {
|
||||
this._config = this._blueprintConfig;
|
||||
if (this._mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
|
||||
}
|
||||
this._blueprintConfig = undefined;
|
||||
this._readOnly = false;
|
||||
}
|
||||
|
||||
private _takeControlSave() {
|
||||
this._readOnly = false;
|
||||
this._dirty = true;
|
||||
this._blueprintConfig = undefined;
|
||||
}
|
||||
|
||||
private async _duplicate() {
|
||||
const result = this._readOnly
|
||||
const result = this.readOnly
|
||||
? await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.picker.migrate_automation"
|
||||
@@ -968,12 +880,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
"ui.panel.config.automation.picker.migrate_automation_description"
|
||||
),
|
||||
})
|
||||
: await this._confirmUnsavedChanged();
|
||||
: await this.confirmUnsavedChanged();
|
||||
if (result) {
|
||||
showAutomationEditor({
|
||||
...this._config,
|
||||
...this.config,
|
||||
id: undefined,
|
||||
alias: this._readOnly ? this._config?.alias : undefined,
|
||||
alias: this.readOnly ? this.config?.alias : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -985,7 +897,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.automation.picker.delete_confirm_text",
|
||||
{ name: this._config?.alias }
|
||||
{ name: this.config?.alias }
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
@@ -1001,43 +913,21 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
}
|
||||
|
||||
private async _switchUiMode() {
|
||||
if (this._yamlErrors) {
|
||||
const result = await showConfirmationDialog(this, {
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.switch_ui_yaml_error"
|
||||
)}<br /><br />${this._yamlErrors}`,
|
||||
confirmText: this.hass!.localize("ui.common.continue"),
|
||||
destructive: true,
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._yamlErrors = undefined;
|
||||
this._mode = "gui";
|
||||
}
|
||||
|
||||
private _switchYamlMode() {
|
||||
this._mode = "yaml";
|
||||
}
|
||||
|
||||
private async _promptAutomationAlias(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
showAutomationSaveDialog(this, {
|
||||
config: this._config!,
|
||||
config: this.config!,
|
||||
domain: "automation",
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this._config = config;
|
||||
this._entityRegistryUpdate = entityRegistryUpdate;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this.requestUpdate();
|
||||
resolve(true);
|
||||
},
|
||||
onClose: () => resolve(false),
|
||||
entityRegistryUpdate: this._entityRegistryUpdate,
|
||||
entityRegistryEntry: this._registryEntry,
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
entityRegistryEntry: this.registryEntry,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1045,10 +935,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
private async _promptAutomationMode(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
showAutomationModeDialog(this, {
|
||||
config: this._config!,
|
||||
config: this.config!,
|
||||
updateConfig: (config) => {
|
||||
this._config = config;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
this.requestUpdate();
|
||||
resolve();
|
||||
},
|
||||
@@ -1058,9 +948,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
|
||||
private async _handleSaveAutomation(): Promise<void> {
|
||||
if (this._yamlErrors) {
|
||||
if (this.yamlErrors) {
|
||||
showToast(this, {
|
||||
message: this._yamlErrors,
|
||||
message: this.yamlErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1082,22 +972,22 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
|
||||
private async _saveAutomation(id): Promise<void> {
|
||||
this._saving = true;
|
||||
this._validationErrors = undefined;
|
||||
this.saving = true;
|
||||
this.validationErrors = undefined;
|
||||
|
||||
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
|
||||
if (this._entityRegistryUpdate !== undefined && !this._entityId) {
|
||||
if (this.entityRegistryUpdate !== undefined && !this.currentEntityId) {
|
||||
this._newAutomationId = id;
|
||||
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
|
||||
this._entityRegCreated = resolve;
|
||||
this.entityRegCreated = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await saveAutomationConfig(this.hass, id, this._config!);
|
||||
await saveAutomationConfig(this.hass, id, this.config!);
|
||||
|
||||
if (this._entityRegistryUpdate !== undefined) {
|
||||
let entityId = this._entityId;
|
||||
if (this.entityRegistryUpdate !== undefined) {
|
||||
let entityId = this.currentEntityId;
|
||||
|
||||
// wait for automation to appear in entity registry when creating a new automation
|
||||
if (entityRegPromise) {
|
||||
@@ -1131,23 +1021,23 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
if (entityId) {
|
||||
await updateEntityRegistryEntry(this.hass, entityId, {
|
||||
categories: {
|
||||
automation: this._entityRegistryUpdate.category || null,
|
||||
automation: this.entityRegistryUpdate.category || null,
|
||||
},
|
||||
labels: this._entityRegistryUpdate.labels || [],
|
||||
area_id: this._entityRegistryUpdate.area || null,
|
||||
labels: this.entityRegistryUpdate.labels || [],
|
||||
area_id: this.entityRegistryUpdate.area || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._dirty = false;
|
||||
this.dirty = false;
|
||||
} catch (errors: any) {
|
||||
this._errors = errors.body?.message || errors.error || errors.body;
|
||||
this.errors = errors.body?.message || errors.error || errors.body;
|
||||
showToast(this, {
|
||||
message: errors.body?.message || errors.error || errors.body,
|
||||
});
|
||||
throw errors;
|
||||
} finally {
|
||||
this._saving = false;
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1157,7 +1047,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
ev.detail.unsub = () => {
|
||||
delete this._configSubscriptions[id];
|
||||
};
|
||||
ev.detail.callback(this._config);
|
||||
ev.detail.callback(this.config);
|
||||
}
|
||||
|
||||
protected supportedShortcuts(): SupportedShortcuts {
|
||||
@@ -1173,14 +1063,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
};
|
||||
}
|
||||
|
||||
protected get isDirty() {
|
||||
return this._dirty;
|
||||
}
|
||||
|
||||
protected async promptDiscardChanges() {
|
||||
return this._confirmUnsavedChanged();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
private _collapseAll() {
|
||||
this._manualEditor?.collapseAll();
|
||||
@@ -1205,8 +1087,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
|
||||
private _applyUndoRedo(config: AutomationConfig) {
|
||||
this._manualEditor?.triggerCloseSidebar();
|
||||
this._config = config;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
private _undo() {
|
||||
@@ -1235,7 +1117,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
this._showInfo();
|
||||
break;
|
||||
case "settings":
|
||||
this._showSettings();
|
||||
this.showSettings();
|
||||
break;
|
||||
case "category":
|
||||
this._editCategory();
|
||||
@@ -1256,11 +1138,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
this._takeControl();
|
||||
break;
|
||||
case "toggle_yaml_mode":
|
||||
if (this._mode === "gui") {
|
||||
this._switchYamlMode();
|
||||
if (this.mode === "gui") {
|
||||
this.switchYamlMode();
|
||||
break;
|
||||
}
|
||||
this._switchUiMode();
|
||||
this.switchUiMode();
|
||||
break;
|
||||
case "disable":
|
||||
this._toggle();
|
||||
@@ -1277,25 +1159,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
automationScriptEditorStyles,
|
||||
css`
|
||||
:host {
|
||||
--ha-automation-editor-max-width: var(
|
||||
--ha-automation-editor-width,
|
||||
1540px
|
||||
);
|
||||
}
|
||||
ha-fade-in {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.yaml-mode {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
manual-automation-editor,
|
||||
blueprint-automation-editor {
|
||||
margin: 0 auto;
|
||||
@@ -1309,17 +1174,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
ha-yaml-editor {
|
||||
flex-grow: 1;
|
||||
--actions-border-radius: var(--ha-border-radius-square);
|
||||
--code-mirror-height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-entity-toggle {
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
@@ -1335,24 +1189,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
max-width: 1040px;
|
||||
padding: 28px 20px 0;
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
right: calc(16px + var(--safe-area-inset-right, 0px));
|
||||
bottom: calc(-80px - var(--safe-area-inset-bottom));
|
||||
transition: bottom 0.3s;
|
||||
}
|
||||
ha-fab.dirty {
|
||||
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
ha-tooltip ha-svg-icon {
|
||||
width: 12px;
|
||||
}
|
||||
ha-tooltip .shortcut {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { CSSResult, TemplateResult, LitElement } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { goBack } from "../../../common/navigate";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import type { Constructor, HomeAssistant, Route } from "../../../types";
|
||||
import type { EntityRegistryUpdate } from "./automation-save-dialog/show-dialog-automation-save";
|
||||
import "../../../components/ha-fade-in";
|
||||
import "../../../components/ha-spinner"; // used by renderLoading() provided to both editors
|
||||
|
||||
/** Minimum config shape shared by both AutomationConfig and ScriptConfig. */
|
||||
interface BaseEditorConfig {
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
/** Shared CSS styles for both automation and script editors. */
|
||||
export const automationScriptEditorStyles: CSSResult = css`
|
||||
:host {
|
||||
--ha-automation-editor-max-width: var(--ha-automation-editor-width, 1540px);
|
||||
}
|
||||
ha-fade-in {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.yaml-mode {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
ha-yaml-editor {
|
||||
flex-grow: 1;
|
||||
--actions-border-radius: var(--ha-border-radius-square);
|
||||
--code-mirror-height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
right: calc(16px + var(--safe-area-inset-right, 0px));
|
||||
bottom: calc(-80px - var(--safe-area-inset-bottom));
|
||||
transition: bottom 0.3s;
|
||||
}
|
||||
ha-fab.dirty {
|
||||
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
ha-tooltip ha-svg-icon {
|
||||
width: 12px;
|
||||
}
|
||||
ha-tooltip .shortcut {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
superClass: Constructor<LitElement>
|
||||
) => {
|
||||
class AutomationScriptEditorClass extends superClass {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public entityId: string | null = null;
|
||||
|
||||
@state() protected dirty = false;
|
||||
|
||||
@state() protected errors?: string;
|
||||
|
||||
@state() protected yamlErrors?: string;
|
||||
|
||||
@state() protected currentEntityId?: string;
|
||||
|
||||
@state() protected mode: "gui" | "yaml" = "gui";
|
||||
|
||||
@state() protected readOnly = false;
|
||||
|
||||
@state() protected saving = false;
|
||||
|
||||
@state() protected validationErrors?: (string | TemplateResult)[];
|
||||
|
||||
@state() protected config?: TConfig;
|
||||
|
||||
@state() protected blueprintConfig?: TConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
|
||||
transformer: function (this: { currentEntityId?: string }, value) {
|
||||
return value.find(
|
||||
({ entity_id }) => entity_id === this.currentEntityId
|
||||
);
|
||||
},
|
||||
watch: ["currentEntityId"],
|
||||
})
|
||||
protected registryEntry?: EntityRegistryEntry;
|
||||
|
||||
protected entityRegistryUpdate?: EntityRegistryUpdate;
|
||||
|
||||
protected entityRegCreated?: (
|
||||
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
|
||||
) => void;
|
||||
|
||||
protected renderLoading(): TemplateResult {
|
||||
return html`
|
||||
<ha-fade-in .delay=${500}>
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-fade-in>
|
||||
`;
|
||||
}
|
||||
|
||||
protected showSettings() {
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: this.currentEntityId!,
|
||||
view: "settings",
|
||||
});
|
||||
}
|
||||
|
||||
protected async switchUiMode() {
|
||||
if (this.yamlErrors) {
|
||||
const result = await showConfirmationDialog(this, {
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.switch_ui_yaml_error"
|
||||
)}<br /><br />${this.yamlErrors}`,
|
||||
confirmText: this.hass!.localize("ui.common.continue"),
|
||||
destructive: true,
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.yamlErrors = undefined;
|
||||
this.mode = "gui";
|
||||
}
|
||||
|
||||
protected switchYamlMode() {
|
||||
this.mode = "yaml";
|
||||
}
|
||||
|
||||
protected takeControlSave() {
|
||||
this.readOnly = false;
|
||||
this.dirty = true;
|
||||
this.blueprintConfig = undefined;
|
||||
}
|
||||
|
||||
protected revertBlueprint() {
|
||||
this.config = this.blueprintConfig;
|
||||
if (this.mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
|
||||
}
|
||||
this.blueprintConfig = undefined;
|
||||
this.readOnly = false;
|
||||
}
|
||||
|
||||
protected backTapped = async () => {
|
||||
const result = await this.confirmUnsavedChanged();
|
||||
if (result) {
|
||||
afterNextRender(() => goBack("/config"));
|
||||
}
|
||||
};
|
||||
|
||||
protected get isDirty() {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
protected async promptDiscardChanges() {
|
||||
return this.confirmUnsavedChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks whether unsaved changes should be discarded.
|
||||
* Subclasses must override this to show a confirmation dialog.
|
||||
* @returns true to proceed (discard/save changes), false to cancel.
|
||||
*/
|
||||
protected confirmUnsavedChanged(): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
return AutomationScriptEditorClass;
|
||||
};
|
||||
@@ -279,7 +279,7 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>`
|
||||
<wa-divider slot="menu-items"></wa-divider>`
|
||||
: nothing}
|
||||
<ha-dropdown-item
|
||||
slot="menu-items"
|
||||
|
||||
@@ -21,6 +21,7 @@ import "../../../components/ha-menu-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tip";
|
||||
import "../../../components/ha-top-app-bar-fixed";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
import type { RepairsIssue } from "../../../data/repairs";
|
||||
import {
|
||||
@@ -211,6 +212,17 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const quickBarLabel = [
|
||||
this.hass.localize("ui.dialogs.quick-bar.title"),
|
||||
this.hass.enableShortcuts && !isMobileClient
|
||||
? isMac
|
||||
? "(⌘ + K)"
|
||||
: "(Ctrl + K)"
|
||||
: undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
const { updates: canInstallUpdates, total: totalUpdates } =
|
||||
this._filterUpdateEntitiesParameterized(
|
||||
this.hass.states,
|
||||
@@ -231,10 +243,15 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
<ha-icon-button
|
||||
slot="actionItems"
|
||||
.label=${this.hass.localize("ui.dialogs.quick-bar.title")}
|
||||
id="button-quick-bar"
|
||||
.label=${quickBarLabel}
|
||||
.path=${mdiMagnify}
|
||||
hide-title
|
||||
@click=${this._showQuickBar}
|
||||
></ha-icon-button>
|
||||
<ha-tooltip placement="bottom" for="button-quick-bar"
|
||||
>${quickBarLabel}</ha-tooltip
|
||||
>
|
||||
<ha-dropdown slot="actionItems" @wa-select=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
|
||||
@@ -86,6 +86,7 @@ class DialogSystemLogDetail extends LitElement {
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
width="large"
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<span slot="headerTitle">${title}</span>
|
||||
@@ -109,6 +110,13 @@ class DialogSystemLogDetail extends LitElement {
|
||||
${item.name}<br />
|
||||
${this.hass.localize("ui.panel.config.logs.detail.source")}:
|
||||
${item.source.join(":")}
|
||||
<br />
|
||||
${this.hass.localize("ui.panel.config.logs.classification")}:
|
||||
${item.error_type
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.logs.error_type.${item.error_type}`
|
||||
)
|
||||
: this.hass.localize("ui.panel.config.logs.other")}
|
||||
${integration
|
||||
? html`
|
||||
<br />
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import {
|
||||
mdiDotsVertical,
|
||||
mdiChevronDown,
|
||||
mdiChip,
|
||||
mdiDns,
|
||||
mdiDownload,
|
||||
mdiFilterVariant,
|
||||
mdiFilterVariantRemove,
|
||||
mdiPackageVariant,
|
||||
mdiPuzzle,
|
||||
mdiRadar,
|
||||
mdiRefresh,
|
||||
mdiText,
|
||||
mdiVolumeHigh,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
@@ -17,10 +23,14 @@ import { navigate } from "../../../common/navigate";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-generic-picker";
|
||||
import "../../../components/ha-icon-button";
|
||||
import type { HaGenericPicker } from "../../../components/ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
|
||||
import "../../../components/search-input";
|
||||
import "../../../components/search-input-outlined";
|
||||
import type { LogProvider } from "../../../data/error_log";
|
||||
import { fetchHassioAddonsInfo } from "../../../data/hassio/addon";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
@@ -28,6 +38,7 @@ import "../../../layouts/hass-subpage";
|
||||
import { mdiHomeAssistant } from "../../../resources/home-assistant-logo-svg";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route, ValueChangedEvent } from "../../../types";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "./error-log-card";
|
||||
import "./system-log-card";
|
||||
import type { SystemLogCard } from "./system-log-card";
|
||||
@@ -81,13 +92,9 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
@state() private _logProviders = logProviders;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const systemLog = this.systemLog;
|
||||
if (systemLog && systemLog.loaded) {
|
||||
systemLog.fetchData();
|
||||
}
|
||||
}
|
||||
@state() private _showSystemLogFilters = false;
|
||||
|
||||
@state() private _systemLogFiltersCount = 0;
|
||||
|
||||
protected firstUpdated(changedProps): void {
|
||||
super.firstUpdated(changedProps);
|
||||
@@ -98,37 +105,140 @@ export class HaConfigLogs extends LitElement {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const search = this.narrow
|
||||
? html`
|
||||
<div slot="header">
|
||||
<search-input
|
||||
class="header"
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></search-input>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></search-input>
|
||||
</div>
|
||||
`;
|
||||
private _toggleSystemLogFilters = () => {
|
||||
this._showSystemLogFilters = !this._showSystemLogFilters;
|
||||
};
|
||||
|
||||
private _handleSystemLogFiltersChanged(ev: CustomEvent) {
|
||||
this._showSystemLogFilters = ev.detail.open;
|
||||
this._systemLogFiltersCount = ev.detail.count;
|
||||
}
|
||||
|
||||
private _downloadSystemLog = () => {
|
||||
this.systemLog?.downloadLogs();
|
||||
};
|
||||
|
||||
private _refreshSystemLog = () => {
|
||||
this.systemLog?.fetchData();
|
||||
};
|
||||
|
||||
private _clearSystemLog = () => {
|
||||
this.systemLog?.clearLogs();
|
||||
};
|
||||
|
||||
private _clearSystemLogFilters = () => {
|
||||
this.systemLog?.clearFilters();
|
||||
};
|
||||
|
||||
private _handleSystemLogOverflowAction(ev: HaDropdownSelectEvent): void {
|
||||
if (ev.detail.item.value === "show-full-logs") {
|
||||
this._showDetail();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const showSystemLog = this._selectedLogProvider === "core" && !this._detail;
|
||||
const selectedProvider = this._getActiveProvider(this._selectedLogProvider);
|
||||
const header =
|
||||
selectedProvider?.primary ||
|
||||
this.hass.localize("ui.panel.config.logs.caption");
|
||||
|
||||
const searchRow = html`
|
||||
<div
|
||||
class="search-row ${showSystemLog
|
||||
? "with-filters"
|
||||
: ""} ${showSystemLog && this._showSystemLogFilters && !this.narrow
|
||||
? "with-pane"
|
||||
: ""}"
|
||||
>
|
||||
${showSystemLog
|
||||
? this._showSystemLogFilters && !this.narrow
|
||||
? html`
|
||||
<div class="filter-controls">
|
||||
<div class="relative filter-button">
|
||||
<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.subpage-data-table.filters"
|
||||
)}
|
||||
active
|
||||
@click=${this._toggleSystemLogFilters}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFilterVariant}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._systemLogFiltersCount
|
||||
? html`<div class="badge">
|
||||
${this._systemLogFiltersCount}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.subpage-data-table.clear_filter"
|
||||
)}
|
||||
.disabled=${!this._systemLogFiltersCount}
|
||||
@click=${this._clearSystemLogFilters}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="relative filter-button">
|
||||
<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.subpage-data-table.filters"
|
||||
)}
|
||||
.active=${this._showSystemLogFilters ||
|
||||
Boolean(this._systemLogFiltersCount)}
|
||||
@click=${this._toggleSystemLogFilters}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFilterVariant}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._systemLogFiltersCount
|
||||
? html`<div class="badge">
|
||||
${this._systemLogFiltersCount}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<search-input-outlined
|
||||
class="search-input"
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
.placeholder=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
@value-changed=${this._filterChanged}
|
||||
></search-input-outlined>
|
||||
|
||||
${showSystemLog
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
class="clear-chip"
|
||||
.label=${this.hass.localize("ui.panel.config.logs.clear")}
|
||||
.disabled=${!this.systemLog?.hasItems}
|
||||
@click=${this._clearSystemLog}
|
||||
></ha-assist-chip>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const search = this.narrow
|
||||
? html`<div slot="header">${searchRow}</div>`
|
||||
: searchRow;
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.logs.caption")}
|
||||
.header=${header}
|
||||
back-path="/config/system"
|
||||
>
|
||||
${isComponentLoaded(this.hass, "hassio") && this._logProviders
|
||||
@@ -164,17 +274,48 @@ export class HaConfigLogs extends LitElement {
|
||||
</ha-generic-picker>
|
||||
`
|
||||
: nothing}
|
||||
${showSystemLog
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.path=${mdiDownload}
|
||||
@click=${this._downloadSystemLog}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.logs.download_logs"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._refreshSystemLog}
|
||||
.label=${this.hass.localize("ui.common.refresh")}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown
|
||||
slot="toolbar-icon"
|
||||
@wa-select=${this._handleSystemLogOverflowAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="show-full-logs">
|
||||
<ha-svg-icon slot="icon" .path=${mdiText}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.logs.show_full_logs")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing}
|
||||
${search}
|
||||
<div class="content">
|
||||
${this._selectedLogProvider === "core" && !this._detail
|
||||
${showSystemLog
|
||||
? html`
|
||||
<system-log-card
|
||||
.hass=${this.hass}
|
||||
.header=${this._logProviders.find(
|
||||
(p) => p.key === this._selectedLogProvider
|
||||
)!.name}
|
||||
.filter=${this._filter}
|
||||
@switch-log-view=${this._showDetail}
|
||||
.showFilters=${this._showSystemLogFilters}
|
||||
@system-log-filters-changed=${this
|
||||
._handleSystemLogFiltersChanged}
|
||||
></system-log-card>
|
||||
`
|
||||
: html`<error-log-card
|
||||
@@ -194,6 +335,7 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
private _showDetail() {
|
||||
this._detail = !this._detail;
|
||||
this._showSystemLogFilters = false;
|
||||
}
|
||||
|
||||
private _openPicker(ev: Event) {
|
||||
@@ -208,6 +350,8 @@ export class HaConfigLogs extends LitElement {
|
||||
}
|
||||
this._selectedLogProvider = provider;
|
||||
this._filter = "";
|
||||
this._showSystemLogFilters = false;
|
||||
this._systemLogFiltersCount = 0;
|
||||
navigate(`/config/logs?provider=${this._selectedLogProvider}`);
|
||||
}
|
||||
|
||||
@@ -342,24 +486,101 @@ export class HaConfigLogs extends LitElement {
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
.search {
|
||||
.search-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 56px;
|
||||
width: 100%;
|
||||
gap: var(--ha-space-4);
|
||||
padding: 0 var(--ha-space-4);
|
||||
background: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
search-input {
|
||||
|
||||
.search-row.with-pane {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
var(--sidepane-width, 250px) minmax(0, 1fr)
|
||||
auto;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-row.with-pane .filter-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 var(--ha-space-4);
|
||||
border-inline-end: 1px solid var(--divider-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-row.with-pane .search-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin-inline-start: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.search-row.with-pane .clear-chip {
|
||||
justify-self: end;
|
||||
margin-inline-start: var(--ha-space-4);
|
||||
margin-inline-end: var(--ha-space-4);
|
||||
}
|
||||
|
||||
search-input-outlined {
|
||||
display: block;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
flex: 1;
|
||||
}
|
||||
search-input.header {
|
||||
--mdc-ripple-color: transparant;
|
||||
margin-left: -16px;
|
||||
margin-inline-start: -16px;
|
||||
margin-inline-end: initial;
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
inset-inline-end: -4px;
|
||||
inset-inline-start: initial;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
.content {
|
||||
direction: ltr;
|
||||
height: calc(
|
||||
100vh -
|
||||
1px - var(--header-height, 0px) - var(
|
||||
--safe-area-inset-top,
|
||||
0px
|
||||
) - var(--safe-area-inset-bottom, 0px) -
|
||||
56px
|
||||
);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
--ha-assist-chip-container-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
.clear-chip {
|
||||
white-space: nowrap;
|
||||
}
|
||||
ha-generic-picker {
|
||||
--md-list-item-leading-icon-color: var(--ha-color-primary-50);
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { mdiDotsVertical, mdiDownload, mdiRefresh, mdiText } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import { getErrorLogDownloadUrl } from "../../../data/error_log";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { LoggedError } from "../../../data/system_log";
|
||||
import type { LoggedError, SystemLogErrorType } from "../../../data/system_log";
|
||||
import {
|
||||
fetchSystemLog,
|
||||
getLoggedErrorIntegration,
|
||||
@@ -25,7 +19,6 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail";
|
||||
import { formatSystemLogTime } from "./util";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
@customElement("system-log-card")
|
||||
export class SystemLogCard extends LitElement {
|
||||
@@ -33,15 +26,45 @@ export class SystemLogCard extends LitElement {
|
||||
|
||||
@property() public filter = "";
|
||||
|
||||
@property() public header?: string;
|
||||
|
||||
public loaded = false;
|
||||
@property({ type: Boolean, attribute: "show-filters" })
|
||||
public showFilters = false;
|
||||
|
||||
@state() private _items?: LoggedError[];
|
||||
|
||||
@state() private _levelFilter: string[] = [];
|
||||
|
||||
@state() private _errorTypeFilter: (SystemLogErrorType | "unknown")[] = [];
|
||||
|
||||
public async fetchData(): Promise<void> {
|
||||
this._items = undefined;
|
||||
this._items = await fetchSystemLog(this.hass!);
|
||||
this._items = await fetchSystemLog(this.hass);
|
||||
}
|
||||
|
||||
public async clearLogs(): Promise<void> {
|
||||
await this.hass.callService("system_log", "clear");
|
||||
this._items = [];
|
||||
}
|
||||
|
||||
public async downloadLogs(): Promise<void> {
|
||||
const timeString = new Date().toISOString().replace(/:/g, "-");
|
||||
const downloadUrl = getErrorLogDownloadUrl(this.hass);
|
||||
const logFileName = `home-assistant_${timeString}.log`;
|
||||
const signedUrl = await getSignedPath(this.hass, downloadUrl);
|
||||
fileDownload(signedUrl.path, logFileName);
|
||||
}
|
||||
|
||||
public get activeFiltersCount(): number {
|
||||
return this._levelFilter.length + this._errorTypeFilter.length;
|
||||
}
|
||||
|
||||
public get hasItems(): boolean {
|
||||
return (this._items?.length || 0) > 0;
|
||||
}
|
||||
|
||||
public clearFilters(): void {
|
||||
this._levelFilter = [];
|
||||
this._errorTypeFilter = [];
|
||||
this._notifyFiltersState();
|
||||
}
|
||||
|
||||
private _timestamp(item: LoggedError): string {
|
||||
@@ -64,8 +87,30 @@ export class SystemLogCard extends LitElement {
|
||||
}
|
||||
|
||||
private _getFilteredItems = memoizeOne(
|
||||
(localize: LocalizeFunc, items: LoggedError[], filter: string) =>
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
items: LoggedError[],
|
||||
filter: string,
|
||||
levelFilter: string[],
|
||||
errorTypeFilter: (SystemLogErrorType | "unknown")[]
|
||||
) =>
|
||||
items.filter((item: LoggedError) => {
|
||||
if (levelFilter.length && !levelFilter.includes(item.level)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (errorTypeFilter.length) {
|
||||
const matchesKnown =
|
||||
item.error_type !== undefined &&
|
||||
errorTypeFilter.includes(item.error_type);
|
||||
const matchesUnknown =
|
||||
item.error_type === undefined &&
|
||||
errorTypeFilter.includes("unknown");
|
||||
if (!matchesKnown && !matchesUnknown) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
const integration = getLoggedErrorIntegration(item);
|
||||
return (
|
||||
@@ -74,6 +119,14 @@ export class SystemLogCard extends LitElement {
|
||||
) ||
|
||||
item.source[0].toLowerCase().includes(filter) ||
|
||||
item.name.toLowerCase().includes(filter) ||
|
||||
(item.error_type &&
|
||||
(item.error_type.includes(filter) ||
|
||||
this.hass
|
||||
.localize(
|
||||
`ui.panel.config.logs.error_type.${item.error_type}`
|
||||
)
|
||||
.toLowerCase()
|
||||
.includes(filter))) ||
|
||||
(integration &&
|
||||
domainToName(localize, integration)
|
||||
.toLowerCase()
|
||||
@@ -82,203 +135,203 @@ export class SystemLogCard extends LitElement {
|
||||
this._multipleMessages(item).toLowerCase().includes(filter)
|
||||
);
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const filteredItems = this._items
|
||||
? this._getFilteredItems(
|
||||
this.hass.localize,
|
||||
this._items,
|
||||
this.filter.toLowerCase()
|
||||
)
|
||||
: [];
|
||||
if (this._items === undefined) {
|
||||
return html`
|
||||
<div class="loading-container">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const filteredItems = this._getFilteredItems(
|
||||
this.hass.localize,
|
||||
this._items,
|
||||
this.filter.toLowerCase(),
|
||||
this._levelFilter,
|
||||
this._errorTypeFilter
|
||||
);
|
||||
|
||||
const levels = [...new Set(this._items.map((item) => item.level))];
|
||||
const errorTypes = [
|
||||
...new Set(
|
||||
this._items
|
||||
.map((item) => item.error_type)
|
||||
.filter((type): type is SystemLogErrorType => Boolean(type))
|
||||
),
|
||||
];
|
||||
|
||||
const integrations = filteredItems.length
|
||||
? filteredItems.map((item) => getLoggedErrorIntegration(item))
|
||||
: [];
|
||||
return html`
|
||||
<div class="system-log-intro">
|
||||
<ha-card outlined>
|
||||
${this._items === undefined
|
||||
? html`
|
||||
<div class="loading-container">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="header">
|
||||
<h1 class="card-header">${this.header || "Logs"}</h1>
|
||||
<div class="header-buttons">
|
||||
<ha-icon-button
|
||||
.path=${mdiDownload}
|
||||
@click=${this._downloadLogs}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.logs.download_logs"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.path=${mdiRefresh}
|
||||
@click=${this.fetchData}
|
||||
.label=${this.hass.localize("ui.common.refresh")}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown @wa-select=${this._handleOverflowAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="show-full-logs">
|
||||
<ha-svg-icon slot="icon" .path=${mdiText}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.logs.show_full_logs"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
${this._items.length === 0
|
||||
? html`
|
||||
<div class="card-content empty-content">
|
||||
${this.hass.localize("ui.panel.config.logs.no_issues")}
|
||||
</div>
|
||||
const hasActiveFilters = this.activeFiltersCount > 0;
|
||||
|
||||
const listContent =
|
||||
this._items.length === 0
|
||||
? html`
|
||||
<div class="card-content empty-content">
|
||||
${this.hass.localize("ui.panel.config.logs.no_issues")}
|
||||
</div>
|
||||
`
|
||||
: filteredItems.length === 0 && (this.filter || hasActiveFilters)
|
||||
? html`
|
||||
<div class="card-content">
|
||||
${this.filter
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.logs.no_issues_search",
|
||||
{
|
||||
term: this.filter,
|
||||
}
|
||||
)
|
||||
: this.hass.localize("ui.panel.config.logs.no_issues")}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="list-wrapper">
|
||||
<ha-list>
|
||||
${filteredItems.map(
|
||||
(item, idx) => html`
|
||||
<ha-list-item
|
||||
@click=${this._openLog}
|
||||
.logItem=${item}
|
||||
twoline
|
||||
>
|
||||
${item.message[0]}
|
||||
<span slot="secondary" class="secondary">
|
||||
${this._timestamp(item)} –
|
||||
${html`(<span class=${item.level}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.logs.level.${item.level}`
|
||||
)}</span
|
||||
>) `}
|
||||
${item.error_type
|
||||
? html`(<span class="error-type-text"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.logs.error_type.${item.error_type}`
|
||||
)}</span
|
||||
>) `
|
||||
: nothing}
|
||||
${integrations[idx]
|
||||
? `${domainToName(
|
||||
this.hass.localize,
|
||||
integrations[idx]!
|
||||
)}${
|
||||
isCustomIntegrationError(item)
|
||||
? ` (${this.hass.localize(
|
||||
"ui.panel.config.logs.custom_integration"
|
||||
)})`
|
||||
: ""
|
||||
}`
|
||||
: item.source[0]}
|
||||
${item.count > 1
|
||||
? html` - ${this._multipleMessages(item)} `
|
||||
: nothing}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
: filteredItems.length === 0 && this.filter
|
||||
? html`<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.logs.no_issues_search",
|
||||
{ term: this.filter }
|
||||
)}
|
||||
</div>`
|
||||
: html`<ha-list
|
||||
>${filteredItems.map(
|
||||
(item, idx) => html`
|
||||
<ha-list-item
|
||||
@click=${this._openLog}
|
||||
.logItem=${item}
|
||||
twoline
|
||||
>
|
||||
${item.message[0]}
|
||||
<span slot="secondary" class="secondary">
|
||||
${this._timestamp(item)} –
|
||||
${html`(<span class=${item.level}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.logs.level.${item.level}`
|
||||
)}</span
|
||||
>) `}
|
||||
${integrations[idx]
|
||||
? `${domainToName(
|
||||
this.hass!.localize,
|
||||
integrations[idx]!
|
||||
)}${
|
||||
isCustomIntegrationError(item)
|
||||
? ` (${this.hass.localize(
|
||||
"ui.panel.config.logs.custom_integration"
|
||||
)})`
|
||||
: ""
|
||||
}`
|
||||
: item.source[0]}
|
||||
${item.count > 1
|
||||
? html` - ${this._multipleMessages(item)} `
|
||||
: nothing}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}</ha-list
|
||||
>`}
|
||||
)}
|
||||
</ha-list>
|
||||
</div>
|
||||
`;
|
||||
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="system_log"
|
||||
service="clear"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.logs.clear"
|
||||
)}</ha-call-service-button
|
||||
>
|
||||
</div>
|
||||
`}
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
return this.showFilters
|
||||
? html`
|
||||
<div class="content-layout">
|
||||
<div class="pane">
|
||||
<div class="pane-content">
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.logs.level_filter"
|
||||
)}
|
||||
.states=${levels.map((level) => ({
|
||||
value: level,
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.logs.level.${level}`
|
||||
),
|
||||
}))}
|
||||
.value=${this._levelFilter}
|
||||
@data-table-filter-changed=${this._levelFilterChanged}
|
||||
></ha-filter-states>
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.logs.classification"
|
||||
)}
|
||||
.states=${[
|
||||
...errorTypes.map((errorType) => ({
|
||||
value: errorType,
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.logs.error_type.${errorType}`
|
||||
),
|
||||
})),
|
||||
{
|
||||
value: "unknown",
|
||||
label: this.hass.localize("ui.panel.config.logs.other"),
|
||||
},
|
||||
]}
|
||||
.value=${this._errorTypeFilter}
|
||||
@data-table-filter-changed=${this._errorTypeFilterChanged}
|
||||
></ha-filter-states>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-main">${listContent}</div>
|
||||
</div>
|
||||
`
|
||||
: listContent;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this.fetchData();
|
||||
this.loaded = true;
|
||||
this.addEventListener("hass-service-called", (ev) =>
|
||||
this.serviceCalled(ev)
|
||||
this._notifyFiltersState();
|
||||
}
|
||||
|
||||
private _levelFilterChanged(ev): void {
|
||||
this._levelFilter = ev.detail.value || [];
|
||||
this._notifyFiltersState();
|
||||
}
|
||||
|
||||
private _errorTypeFilterChanged(ev): void {
|
||||
this._errorTypeFilter = ev.detail.value || [];
|
||||
this._notifyFiltersState();
|
||||
}
|
||||
|
||||
private _notifyFiltersState(): void {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("system-log-filters-changed", {
|
||||
detail: {
|
||||
open: this.showFilters,
|
||||
count: this.activeFiltersCount,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected serviceCalled(ev): void {
|
||||
// Check if this is for us
|
||||
if (ev.detail.success && ev.detail.domain === "system_log") {
|
||||
// Do the right thing depending on service
|
||||
if (ev.detail.service === "clear") {
|
||||
this._items = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleOverflowAction(ev: HaDropdownSelectEvent) {
|
||||
if (ev.detail.item.value === "show-full-logs") {
|
||||
// @ts-ignore
|
||||
fireEvent(this, "switch-log-view");
|
||||
}
|
||||
}
|
||||
|
||||
private async _downloadLogs() {
|
||||
const timeString = new Date().toISOString().replace(/:/g, "-");
|
||||
const downloadUrl = getErrorLogDownloadUrl(this.hass);
|
||||
const logFileName = `home-assistant_${timeString}.log`;
|
||||
const signedUrl = await getSignedPath(this.hass, downloadUrl);
|
||||
fileDownload(signedUrl.path, logFileName);
|
||||
}
|
||||
|
||||
private _openLog(ev: Event): void {
|
||||
const item = (ev.currentTarget as any).logItem;
|
||||
showSystemLogDetailDialog(this, { item });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
padding-top: 8px;
|
||||
:host {
|
||||
display: block;
|
||||
direction: var(--direction);
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
|
||||
:host {
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-list {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
|
||||
letter-spacing: -0.012em;
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
display: block;
|
||||
margin-block-start: 0px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
|
||||
.system-log-intro {
|
||||
margin: 16px;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
@@ -288,6 +341,39 @@ export class SystemLogCard extends LitElement {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-layout {
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
min-height: 100%;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
|
||||
.pane {
|
||||
flex: 0 0 var(--sidepane-width, 250px);
|
||||
width: var(--sidepane-width, 250px);
|
||||
border-inline-end: 1px solid var(--divider-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
|
||||
.pane-content {
|
||||
overflow: auto;
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
@@ -296,15 +382,33 @@ export class SystemLogCard extends LitElement {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.error-type-text {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
min-height: 100%;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
|
||||
.row-secondary {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.content-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pane {
|
||||
width: 100%;
|
||||
border-inline-end: none;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -143,8 +143,7 @@ class HaConfigRepairs extends LitElement {
|
||||
}
|
||||
} else if (
|
||||
issue.domain === "vacuum" &&
|
||||
(issue.translation_key === "segments_changed" ||
|
||||
issue.translation_key === "segments_mapping_not_configured")
|
||||
issue.translation_key === "segments_changed"
|
||||
) {
|
||||
const data = await fetchRepairsIssueData(
|
||||
this.hass.connection,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiCog,
|
||||
@@ -22,15 +21,13 @@ import {
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import { promiseTimeout } from "../../../common/util/promise-timeout";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
@@ -40,7 +37,6 @@ import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import { substituteBlueprint } from "../../../data/blueprint";
|
||||
import { validateConfig } from "../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import {
|
||||
type EntityRegistryEntry,
|
||||
@@ -67,88 +63,47 @@ import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { Entries, HomeAssistant, Route } from "../../../types";
|
||||
import type { Entries } from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showAutomationModeDialog } from "../automation/automation-mode-dialog/show-dialog-automation-mode";
|
||||
import type { EntityRegistryUpdate } from "../automation/automation-save-dialog/show-dialog-automation-save";
|
||||
import { showAutomationSaveDialog } from "../automation/automation-save-dialog/show-dialog-automation-save";
|
||||
import { showAutomationSaveTimeoutDialog } from "../automation/automation-save-timeout-dialog/show-dialog-automation-save-timeout";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import "./blueprint-script-editor";
|
||||
import {
|
||||
AutomationScriptEditorMixin,
|
||||
automationScriptEditorStyles,
|
||||
} from "../automation/ha-automation-script-editor-mixin";
|
||||
import "./manual-script-editor";
|
||||
import type { HaManualScriptEditor } from "./manual-script-editor";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
@customElement("ha-script-editor")
|
||||
export class HaScriptEditor extends SubscribeMixin(
|
||||
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
|
||||
AutomationScriptEditorMixin<ScriptConfig>(
|
||||
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
|
||||
)
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public scriptId: string | null = null;
|
||||
|
||||
@property({ attribute: false }) public entityId: string | null = null;
|
||||
|
||||
@property({ attribute: false }) public entityRegistry!: EntityRegistryEntry[];
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _config?: ScriptConfig;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _errors?: string;
|
||||
|
||||
@state() private _yamlErrors?: string;
|
||||
|
||||
@state() private _entityId?: string;
|
||||
|
||||
@state() private _mode: "gui" | "yaml" = "gui";
|
||||
|
||||
@state() private _readOnly = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
|
||||
transformer: function (this: HaScriptEditor, value) {
|
||||
return value.find(({ entity_id }) => entity_id === this._entityId);
|
||||
},
|
||||
watch: ["_entityId"],
|
||||
})
|
||||
private _registryEntry?: EntityRegistryEntry;
|
||||
|
||||
@query("manual-script-editor")
|
||||
private _manualEditor?: HaManualScriptEditor;
|
||||
|
||||
@state() private _validationErrors?: (string | TemplateResult)[];
|
||||
|
||||
@state() private _blueprintConfig?: BlueprintScriptConfig;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
private _entityRegistryUpdate?: EntityRegistryUpdate;
|
||||
|
||||
private _newScriptId?: string;
|
||||
|
||||
private _entityRegCreated?: (
|
||||
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
|
||||
) => void;
|
||||
|
||||
private _undoRedoController = new UndoRedoController<ScriptConfig>(this, {
|
||||
apply: (config) => this._applyUndoRedo(config),
|
||||
currentConfig: () => this._config!,
|
||||
currentConfig: () => this.config!,
|
||||
});
|
||||
|
||||
protected willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (
|
||||
this._entityRegCreated &&
|
||||
this.entityRegCreated &&
|
||||
this._newScriptId &&
|
||||
changedProps.has("entityRegistry")
|
||||
) {
|
||||
@@ -157,22 +112,22 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
entity.platform === "script" && entity.unique_id === this._newScriptId
|
||||
);
|
||||
if (script) {
|
||||
this._entityRegCreated(script);
|
||||
this._entityRegCreated = undefined;
|
||||
this.entityRegCreated(script);
|
||||
this.entityRegCreated = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._config) {
|
||||
return nothing;
|
||||
if (!this.config) {
|
||||
return this.renderLoading();
|
||||
}
|
||||
|
||||
const stateObj = this._entityId
|
||||
? this.hass.states[this._entityId]
|
||||
const stateObj = this.currentEntityId
|
||||
? this.hass.states[this.currentEntityId]
|
||||
: undefined;
|
||||
|
||||
const useBlueprint = "use_blueprint" in this._config;
|
||||
const useBlueprint = "use_blueprint" in this.config;
|
||||
const shortcutIcon = isMac
|
||||
? html`<ha-svg-icon .path=${mdiAppleKeyboardCommand}></ha-svg-icon>`
|
||||
: this.hass.localize("ui.panel.config.automation.editor.ctrl");
|
||||
@@ -182,11 +137,11 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.backCallback=${this._backTapped}
|
||||
.header=${this._config.alias ||
|
||||
.backCallback=${this.backTapped}
|
||||
.header=${this.config.alias ||
|
||||
this.hass.localize("ui.panel.config.script.editor.default_name")}
|
||||
>
|
||||
${this._mode === "gui" && !this.narrow
|
||||
${this.mode === "gui" && !this.narrow
|
||||
? html`<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.label=${this.hass.localize("ui.common.undo")}
|
||||
@@ -252,7 +207,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${this._mode === "gui" && this.narrow
|
||||
${this.mode === "gui" && this.narrow
|
||||
? html`<ha-dropdown-item
|
||||
value="undo"
|
||||
.disabled=${!this._undoRedoController.canUndo}
|
||||
@@ -286,7 +241,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
<ha-dropdown-item .disabled=${!stateObj} value="category">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.scene.picker.${this._registryEntry?.categories?.script ? "edit_category" : "assign_category"}`
|
||||
`ui.panel.config.scene.picker.${this.registryEntry?.categories?.script ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
@@ -307,10 +262,10 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
></ha-svg-icon>
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${!useBlueprint && !("fields" in this._config)
|
||||
${!useBlueprint && !("fields" in this.config)
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
.disabled=${this._readOnly || this._mode === "yaml"}
|
||||
.disabled=${this.readOnly || this.mode === "yaml"}
|
||||
value="add_fields"
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -326,9 +281,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
<ha-dropdown-item
|
||||
value="rename"
|
||||
.disabled=${!this.scriptId ||
|
||||
this._readOnly ||
|
||||
this._mode === "yaml"}
|
||||
.disabled=${!this.scriptId || this.readOnly || this.mode === "yaml"}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.script.editor.rename")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
@@ -337,7 +290,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
value="change_mode"
|
||||
.disabled=${this._readOnly || this._mode === "yaml"}
|
||||
.disabled=${this.readOnly || this.mode === "yaml"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.change_mode"
|
||||
@@ -351,12 +304,12 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
: nothing}
|
||||
|
||||
<ha-dropdown-item
|
||||
.disabled=${!!this._blueprintConfig ||
|
||||
(!this._readOnly && !this.scriptId)}
|
||||
.disabled=${!!this.blueprintConfig ||
|
||||
(!this.readOnly && !this.scriptId)}
|
||||
value="duplicate"
|
||||
>
|
||||
${this.hass.localize(
|
||||
this._readOnly
|
||||
this.readOnly
|
||||
? "ui.panel.config.script.editor.migrate"
|
||||
: "ui.panel.config.script.editor.duplicate"
|
||||
)}
|
||||
@@ -370,7 +323,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
value="take_control"
|
||||
.disabled=${this._readOnly}
|
||||
.disabled=${this.readOnly}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.take_control"
|
||||
@@ -382,7 +335,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
<ha-dropdown-item value="toggle_yaml_mode">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${this._mode === "gui" ? "yaml" : "ui"}`
|
||||
`ui.panel.config.automation.editor.edit_${this.mode === "gui" ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
@@ -390,7 +343,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<ha-dropdown-item
|
||||
.disabled=${this._readOnly || !this.scriptId}
|
||||
.disabled=${this.readOnly || !this.scriptId}
|
||||
value="delete"
|
||||
.variant=${this.scriptId ? "danger" : "default"}
|
||||
>
|
||||
@@ -403,8 +356,8 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
</ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<div class=${this._mode === "yaml" ? "yaml-mode" : ""}>
|
||||
${this._mode === "gui"
|
||||
<div class=${this.mode === "yaml" ? "yaml-mode" : ""}>
|
||||
${this.mode === "gui"
|
||||
? html`
|
||||
<div>
|
||||
${useBlueprint
|
||||
@@ -413,10 +366,10 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.config=${this._config}
|
||||
.disabled=${this._readOnly}
|
||||
.saving=${this._saving}
|
||||
.dirty=${this._dirty}
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.saving=${this.saving}
|
||||
.dirty=${this.dirty}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-script=${this._handleSaveScript}
|
||||
></blueprint-script-editor>
|
||||
@@ -426,16 +379,16 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.config=${this._config}
|
||||
.disabled=${this._readOnly}
|
||||
.dirty=${this._dirty}
|
||||
.saving=${this._saving}
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.dirty=${this.dirty}
|
||||
.saving=${this.saving}
|
||||
@value-changed=${this._valueChanged}
|
||||
@editor-save=${this._handleSaveScript}
|
||||
@save-script=${this._handleSaveScript}
|
||||
>
|
||||
<div class="alert-wrapper" slot="alerts">
|
||||
${this._errors || stateObj?.state === UNAVAILABLE
|
||||
${this.errors || stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
.title=${stateObj?.state === UNAVAILABLE
|
||||
@@ -444,7 +397,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
)
|
||||
: undefined}
|
||||
>
|
||||
${this._errors || this._validationErrors}
|
||||
${this.errors || this.validationErrors}
|
||||
${stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-svg-icon
|
||||
slot="icon"
|
||||
@@ -453,7 +406,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${this._blueprintConfig
|
||||
${this.blueprintConfig
|
||||
? html`<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.confirm_take_control"
|
||||
@@ -461,21 +414,21 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
<div slot="action" style="display: flex;">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._takeControlSave}
|
||||
@click=${this.takeControlSave}
|
||||
>${this.hass.localize(
|
||||
"ui.common.yes"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._revertBlueprint}
|
||||
@click=${this.revertBlueprint}
|
||||
>${this.hass.localize(
|
||||
"ui.common.no"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-alert>`
|
||||
: this._readOnly
|
||||
: this.readOnly
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
dismissable
|
||||
@@ -498,11 +451,11 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: this._mode === "yaml"
|
||||
: this.mode === "yaml"
|
||||
? html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
.readOnly=${this._readOnly}
|
||||
.readOnly=${this.readOnly}
|
||||
disable-fullscreen
|
||||
@value-changed=${this._yamlChanged}
|
||||
@editor-save=${this._handleSaveScript}
|
||||
@@ -510,9 +463,9 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
></ha-yaml-editor>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${!this._readOnly && this._dirty ? "dirty" : ""}
|
||||
class=${!this.readOnly && this.dirty ? "dirty" : ""}
|
||||
.label=${this.hass.localize("ui.common.save")}
|
||||
.disabled=${this._saving}
|
||||
.disabled=${this.saving}
|
||||
extended
|
||||
@click=${this._handleSaveScript}
|
||||
>
|
||||
@@ -551,26 +504,26 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
const entity = this.entityRegistry.find(
|
||||
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
|
||||
);
|
||||
this._entityId = entity?.entity_id;
|
||||
this.currentEntityId = entity?.entity_id;
|
||||
}
|
||||
|
||||
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
|
||||
const initData = getScriptEditorInitData();
|
||||
this._dirty = !!initData;
|
||||
this.dirty = !!initData;
|
||||
const baseConfig: Partial<ScriptConfig> = {};
|
||||
if (!initData || !("use_blueprint" in initData)) {
|
||||
baseConfig.sequence = [];
|
||||
}
|
||||
this._config = {
|
||||
this.config = {
|
||||
...baseConfig,
|
||||
...initData,
|
||||
} as ScriptConfig;
|
||||
this._readOnly = false;
|
||||
this.readOnly = false;
|
||||
}
|
||||
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
getScriptStateConfig(this.hass, this.entityId).then((c) => {
|
||||
this._config = normalizeScriptConfig(c.config);
|
||||
this.config = normalizeScriptConfig(c.config);
|
||||
this._checkValidation();
|
||||
});
|
||||
const regEntry = this.entityRegistry.find(
|
||||
@@ -579,25 +532,25 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
if (regEntry?.unique_id) {
|
||||
this.scriptId = regEntry.unique_id;
|
||||
}
|
||||
this._entityId = this.entityId;
|
||||
this._dirty = false;
|
||||
this._readOnly = true;
|
||||
this.currentEntityId = this.entityId;
|
||||
this.dirty = false;
|
||||
this.readOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async _checkValidation() {
|
||||
this._validationErrors = undefined;
|
||||
if (!this._entityId || !this._config) {
|
||||
this.validationErrors = undefined;
|
||||
if (!this.currentEntityId || !this.config) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass.states[this._entityId];
|
||||
const stateObj = this.hass.states[this.currentEntityId];
|
||||
if (stateObj?.state !== UNAVAILABLE) {
|
||||
return;
|
||||
}
|
||||
const validation = await validateConfig(this.hass, {
|
||||
actions: this._config.sequence,
|
||||
actions: this.config.sequence,
|
||||
});
|
||||
this._validationErrors = (
|
||||
this.validationErrors = (
|
||||
Object.entries(validation) as Entries<typeof validation>
|
||||
).map(([key, value]) =>
|
||||
value.valid
|
||||
@@ -612,13 +565,13 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
private async _loadConfig() {
|
||||
fetchScriptFileConfig(this.hass, this.scriptId!).then(
|
||||
(config) => {
|
||||
this._dirty = false;
|
||||
this._readOnly = false;
|
||||
this._config = normalizeScriptConfig(config);
|
||||
this.dirty = false;
|
||||
this.readOnly = false;
|
||||
this.config = normalizeScriptConfig(config);
|
||||
const entity = this.entityRegistry.find(
|
||||
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
|
||||
);
|
||||
this._entityId = entity?.entity_id;
|
||||
this.currentEntityId = entity?.entity_id;
|
||||
this._checkValidation();
|
||||
},
|
||||
(resp) => {
|
||||
@@ -647,19 +600,19 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
if (this._config) {
|
||||
this._undoRedoController.commit(this._config);
|
||||
if (this.config) {
|
||||
this._undoRedoController.commit(this.config);
|
||||
}
|
||||
|
||||
this._config = ev.detail.value;
|
||||
this._errors = undefined;
|
||||
this._dirty = true;
|
||||
this.config = ev.detail.value;
|
||||
this.errors = undefined;
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
private async _runScript() {
|
||||
if (hasScriptFields(this.hass, this._entityId!)) {
|
||||
if (hasScriptFields(this.hass, this.currentEntityId!)) {
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: this._entityId!,
|
||||
entityId: this.currentEntityId!,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -667,20 +620,13 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
await triggerScript(this.hass, this.scriptId!);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.notification_toast.triggered", {
|
||||
name: this._config!.alias,
|
||||
name: this.config!.alias,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private _showSettings() {
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: this._entityId!,
|
||||
view: "settings",
|
||||
});
|
||||
}
|
||||
|
||||
private _editCategory() {
|
||||
if (!this._registryEntry) {
|
||||
if (!this.registryEntry) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.scene.picker.no_category_support"
|
||||
@@ -693,7 +639,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
showAssignCategoryDialog(this, {
|
||||
scope: "script",
|
||||
entityReg: this._registryEntry,
|
||||
entityReg: this.registryEntry,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -730,7 +676,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
private async _showTrace() {
|
||||
if (this.scriptId) {
|
||||
const result = await this._confirmUnsavedChanged();
|
||||
const result = await this.confirmUnsavedChanged();
|
||||
if (result) {
|
||||
navigate(`/config/script/trace/${this.scriptId}`);
|
||||
}
|
||||
@@ -738,47 +684,47 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
|
||||
private _addFields() {
|
||||
if ("fields" in this._config!) {
|
||||
if ("fields" in this.config!) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._config) {
|
||||
this._undoRedoController.commit(this._config);
|
||||
if (this.config) {
|
||||
this._undoRedoController.commit(this.config);
|
||||
}
|
||||
|
||||
this._manualEditor?.addFields();
|
||||
this._dirty = true;
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
private _preprocessYaml() {
|
||||
return this._config;
|
||||
return this.config;
|
||||
}
|
||||
|
||||
private _yamlChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._dirty = true;
|
||||
this.dirty = true;
|
||||
if (!ev.detail.isValid) {
|
||||
this._yamlErrors = ev.detail.errorMsg;
|
||||
this.yamlErrors = ev.detail.errorMsg;
|
||||
return;
|
||||
}
|
||||
this._yamlErrors = undefined;
|
||||
this._config = ev.detail.value;
|
||||
this._errors = undefined;
|
||||
this.yamlErrors = undefined;
|
||||
this.config = ev.detail.value;
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
private async _confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (!this._dirty) {
|
||||
protected async confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (!this.dirty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
showAutomationSaveDialog(this, {
|
||||
config: this._config!,
|
||||
config: this.config!,
|
||||
domain: "script",
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this._config = config;
|
||||
this._entityRegistryUpdate = entityRegistryUpdate;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this.requestUpdate();
|
||||
|
||||
const id = this.scriptId || String(Date.now());
|
||||
@@ -794,8 +740,8 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
},
|
||||
onClose: () => resolve(false),
|
||||
onDiscard: () => resolve(true),
|
||||
entityRegistryUpdate: this._entityRegistryUpdate,
|
||||
entityRegistryEntry: this._registryEntry,
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
entityRegistryEntry: this.registryEntry,
|
||||
title: this.hass.localize(
|
||||
this.scriptId
|
||||
? "ui.panel.config.script.editor.leave.unsaved_confirm_title"
|
||||
@@ -811,15 +757,8 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
});
|
||||
}
|
||||
|
||||
private _backTapped = async () => {
|
||||
const result = await this._confirmUnsavedChanged();
|
||||
if (result) {
|
||||
afterNextRender(() => goBack("/config"));
|
||||
}
|
||||
};
|
||||
|
||||
private async _takeControl() {
|
||||
const config = this._config as BlueprintScriptConfig;
|
||||
const config = this.config as BlueprintScriptConfig;
|
||||
|
||||
try {
|
||||
const result = await substituteBlueprint(
|
||||
@@ -835,35 +774,20 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
description: config.description,
|
||||
};
|
||||
|
||||
this._blueprintConfig = config;
|
||||
this._config = newConfig;
|
||||
if (this._mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
|
||||
this.blueprintConfig = config;
|
||||
this.config = newConfig;
|
||||
if (this.mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
|
||||
}
|
||||
this._readOnly = true;
|
||||
this._errors = undefined;
|
||||
this.readOnly = true;
|
||||
this.errors = undefined;
|
||||
} catch (err: any) {
|
||||
this._errors = err.message;
|
||||
this.errors = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
private _revertBlueprint() {
|
||||
this._config = this._blueprintConfig;
|
||||
if (this._mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
|
||||
}
|
||||
this._blueprintConfig = undefined;
|
||||
this._readOnly = false;
|
||||
}
|
||||
|
||||
private _takeControlSave() {
|
||||
this._readOnly = false;
|
||||
this._dirty = true;
|
||||
this._blueprintConfig = undefined;
|
||||
}
|
||||
|
||||
private async _duplicate() {
|
||||
const result = this._readOnly
|
||||
const result = this.readOnly
|
||||
? await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.script.picker.migrate_script"
|
||||
@@ -872,14 +796,14 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
"ui.panel.config.script.picker.migrate_script_description"
|
||||
),
|
||||
})
|
||||
: await this._confirmUnsavedChanged();
|
||||
: await this.confirmUnsavedChanged();
|
||||
if (result) {
|
||||
this._entityId = undefined;
|
||||
this.currentEntityId = undefined;
|
||||
showScriptEditor({
|
||||
...this._config,
|
||||
alias: this._readOnly
|
||||
? this._config?.alias
|
||||
: `${this._config?.alias} (${this.hass.localize(
|
||||
...this.config,
|
||||
alias: this.readOnly
|
||||
? this.config?.alias
|
||||
: `${this.config?.alias} (${this.hass.localize(
|
||||
"ui.panel.config.script.picker.duplicate"
|
||||
)})`,
|
||||
});
|
||||
@@ -893,7 +817,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.script.editor.delete_confirm_text",
|
||||
{ name: this._config?.alias }
|
||||
{ name: this.config?.alias }
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
@@ -907,42 +831,20 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
goBack("/config");
|
||||
}
|
||||
|
||||
private async _switchUiMode() {
|
||||
if (this._yamlErrors) {
|
||||
const result = await showConfirmationDialog(this, {
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.switch_ui_yaml_error"
|
||||
)}<br /><br />${this._yamlErrors}`,
|
||||
confirmText: this.hass!.localize("ui.common.continue"),
|
||||
destructive: true,
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._yamlErrors = undefined;
|
||||
this._mode = "gui";
|
||||
}
|
||||
|
||||
private _switchYamlMode() {
|
||||
this._mode = "yaml";
|
||||
}
|
||||
|
||||
private async _promptScriptAlias(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
showAutomationSaveDialog(this, {
|
||||
config: this._config!,
|
||||
config: this.config!,
|
||||
domain: "script",
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this._config = config;
|
||||
this._entityRegistryUpdate = entityRegistryUpdate;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this.requestUpdate();
|
||||
resolve(true);
|
||||
},
|
||||
onClose: () => resolve(false),
|
||||
entityRegistryUpdate: this._entityRegistryUpdate,
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
entityRegistryEntry: this.entityRegistry.find(
|
||||
(entry) => entry.unique_id === this.scriptId
|
||||
),
|
||||
@@ -953,10 +855,10 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
private async _promptScriptMode(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
showAutomationModeDialog(this, {
|
||||
config: this._config!,
|
||||
config: this.config!,
|
||||
updateConfig: (config) => {
|
||||
this._config = config;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
this.requestUpdate();
|
||||
resolve();
|
||||
},
|
||||
@@ -966,9 +868,9 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
|
||||
private async _handleSaveScript() {
|
||||
if (this._yamlErrors) {
|
||||
if (this.yamlErrors) {
|
||||
showToast(this, {
|
||||
message: this._yamlErrors,
|
||||
message: this.yamlErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -980,9 +882,9 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
if (!saved) {
|
||||
return;
|
||||
}
|
||||
this._entityId = this._computeEntityIdFromAlias(this._config!.alias);
|
||||
this.currentEntityId = this._computeEntityIdFromAlias(this.config!.alias);
|
||||
}
|
||||
const id = this.scriptId || this._entityId || Date.now();
|
||||
const id = this.scriptId || this.currentEntityId || Date.now();
|
||||
|
||||
await this._saveScript(id);
|
||||
if (!this.scriptId) {
|
||||
@@ -991,13 +893,13 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
|
||||
private async _saveScript(id): Promise<void> {
|
||||
this._saving = true;
|
||||
this.saving = true;
|
||||
|
||||
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
|
||||
if (this._entityRegistryUpdate !== undefined && !this.scriptId) {
|
||||
if (this.entityRegistryUpdate !== undefined && !this.scriptId) {
|
||||
this._newScriptId = id.toString();
|
||||
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
|
||||
this._entityRegCreated = resolve;
|
||||
this.entityRegCreated = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1005,11 +907,11 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
await this.hass!.callApi(
|
||||
"POST",
|
||||
"config/script/config/" + id,
|
||||
this._config
|
||||
this.config
|
||||
);
|
||||
|
||||
if (this._entityRegistryUpdate !== undefined) {
|
||||
let entityId = this._entityId;
|
||||
if (this.entityRegistryUpdate !== undefined) {
|
||||
let entityId = this.currentEntityId;
|
||||
|
||||
// wait for new script to appear in entity registry
|
||||
if (entityRegPromise) {
|
||||
@@ -1044,23 +946,23 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
if (entityId) {
|
||||
await updateEntityRegistryEntry(this.hass, entityId, {
|
||||
categories: {
|
||||
script: this._entityRegistryUpdate.category || null,
|
||||
script: this.entityRegistryUpdate.category || null,
|
||||
},
|
||||
labels: this._entityRegistryUpdate.labels || [],
|
||||
area_id: this._entityRegistryUpdate.area || null,
|
||||
labels: this.entityRegistryUpdate.labels || [],
|
||||
area_id: this.entityRegistryUpdate.area || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._dirty = false;
|
||||
this.dirty = false;
|
||||
} catch (errors: any) {
|
||||
this._errors = errors.body?.message || errors.error || errors.body;
|
||||
this.errors = errors.body?.message || errors.error || errors.body;
|
||||
showToast(this, {
|
||||
message: errors.body?.message || errors.error || errors.body,
|
||||
});
|
||||
throw errors;
|
||||
} finally {
|
||||
this._saving = false;
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1077,14 +979,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
};
|
||||
}
|
||||
|
||||
protected get isDirty() {
|
||||
return this._dirty;
|
||||
}
|
||||
|
||||
protected async promptDiscardChanges() {
|
||||
return this._confirmUnsavedChanged();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
private _collapseAll() {
|
||||
this._manualEditor?.collapseAll();
|
||||
@@ -1109,8 +1003,8 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
private _applyUndoRedo(config: ScriptConfig) {
|
||||
this._manualEditor?.triggerCloseSidebar();
|
||||
this._config = config;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
private _undo() {
|
||||
@@ -1139,7 +1033,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
this._showInfo();
|
||||
break;
|
||||
case "settings":
|
||||
this._showSettings();
|
||||
this.showSettings();
|
||||
break;
|
||||
case "category":
|
||||
this._editCategory();
|
||||
@@ -1163,11 +1057,11 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
this._takeControl();
|
||||
break;
|
||||
case "toggle_yaml_mode":
|
||||
if (this._mode === "gui") {
|
||||
this._switchYamlMode();
|
||||
if (this.mode === "gui") {
|
||||
this.switchYamlMode();
|
||||
break;
|
||||
}
|
||||
this._switchUiMode();
|
||||
this.switchUiMode();
|
||||
break;
|
||||
case "delete":
|
||||
this._deleteConfirm();
|
||||
@@ -1181,19 +1075,8 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
automationScriptEditorStyles,
|
||||
css`
|
||||
:host {
|
||||
--ha-automation-editor-max-width: var(
|
||||
--ha-automation-editor-width,
|
||||
1540px
|
||||
);
|
||||
}
|
||||
.yaml-mode {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
manual-script-editor,
|
||||
blueprint-script-editor {
|
||||
margin: 0 auto;
|
||||
@@ -1244,29 +1127,9 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
ha-yaml-editor {
|
||||
flex-grow: 1;
|
||||
--actions-border-radius: var(--ha-border-radius-square);
|
||||
--code-mirror-height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
span[slot="introduction"] a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: calc(-80px - var(--safe-area-inset-bottom));
|
||||
transition: bottom 0.3s;
|
||||
}
|
||||
ha-fab.dirty {
|
||||
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
margin: 16px 0;
|
||||
@@ -1280,15 +1143,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.header a {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-tooltip ha-svg-icon {
|
||||
width: 12px;
|
||||
}
|
||||
ha-tooltip .shortcut {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -670,10 +670,10 @@ export class AssistPipelineDebug extends LitElement {
|
||||
background-color: var(--light-primary-color);
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
direction: var(--direction);
|
||||
--primary-text-color: var(
|
||||
--text-light-primary-color,
|
||||
var(--primary-text-color)
|
||||
);
|
||||
}
|
||||
|
||||
.tool_result [slot="header"] {
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
}
|
||||
|
||||
.message.user,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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";
|
||||
@@ -11,13 +10,9 @@ import "../../components/ha-top-app-bar-fixed";
|
||||
import type { EnergyPreferences } from "../../data/energy";
|
||||
import { getEnergyDataCollection } 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 { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo } from "../../types";
|
||||
import "../lovelace/components/hui-energy-period-selector";
|
||||
import "../lovelace/hui-root";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
@@ -37,7 +32,6 @@ const OVERVIEW_VIEW = {
|
||||
strategy: {
|
||||
type: "energy-overview",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
show_period_selector: true,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
|
||||
@@ -46,7 +40,6 @@ const ENERGY_VIEW = {
|
||||
strategy: {
|
||||
type: "energy",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
show_period_selector: true,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
|
||||
@@ -55,7 +48,6 @@ const WATER_VIEW = {
|
||||
strategy: {
|
||||
type: "water",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
show_period_selector: true,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
|
||||
@@ -64,7 +56,6 @@ const GAS_VIEW = {
|
||||
strategy: {
|
||||
type: "gas",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
show_period_selector: true,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
|
||||
@@ -189,7 +180,9 @@ class PanelEnergy extends LitElement {
|
||||
return html`
|
||||
<div class="centered">
|
||||
<ha-alert alert-type="error">
|
||||
An error occurred loading energy preferences: ${this._error}
|
||||
${this.hass.localize("ui.panel.energy.error_loading_preferences", {
|
||||
error: this._error,
|
||||
})}
|
||||
</ha-alert>
|
||||
</div>
|
||||
`;
|
||||
@@ -208,16 +201,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}
|
||||
@@ -228,22 +211,8 @@ class PanelEnergy extends LitElement {
|
||||
.backButton=${this._searchParms.has("historyBack")}
|
||||
.backPath=${this._searchParms.get("backPath") || "/"}
|
||||
@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}
|
||||
opening-direction="right"
|
||||
vertical-opening-direction="up"
|
||||
fixed
|
||||
></hui-energy-period-selector>
|
||||
</ha-card>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -354,50 +323,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);
|
||||
transition:
|
||||
left var(--ha-animation-duration-normal) ease,
|
||||
right var(--ha-animation-duration-normal) ease,
|
||||
inset-inline-start var(--ha-animation-duration-normal) ease,
|
||||
inset-inline-end var(--ha-animation-duration-normal) ease;
|
||||
margin: 0 auto;
|
||||
max-width: calc(min(470px, 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));
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -13,16 +13,22 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
sections: [],
|
||||
dense_section_placement: true,
|
||||
max_columns: 2,
|
||||
max_columns: 3,
|
||||
footer: {
|
||||
card: {
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
|
||||
const energyCollection = getEnergyDataCollection(hass, {
|
||||
key: collectionKey,
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { GridSourceTypeEnergyPreference } from "../../../data/energy";
|
||||
import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -14,11 +15,21 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const view: LovelaceViewConfig = { cards: [] };
|
||||
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
max_columns: 3,
|
||||
sections: [],
|
||||
footer: {
|
||||
card: {
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const energyCollection = getEnergyDataCollection(hass, {
|
||||
key: collectionKey,
|
||||
});
|
||||
@@ -36,8 +47,6 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
return view;
|
||||
}
|
||||
|
||||
view.type = "sidebar";
|
||||
|
||||
const hasGrid = prefs.energy_sources.find(
|
||||
(source): source is GridSourceTypeEnergyPreference =>
|
||||
source.type === "grid" &&
|
||||
@@ -50,83 +59,95 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
const hasBattery = prefs.energy_sources.some(
|
||||
(source) => source.type === "battery"
|
||||
);
|
||||
view.cards!.push({
|
||||
|
||||
const mainCards: LovelaceCardConfig[] = [];
|
||||
const gaugeCards: LovelaceCardConfig[] = [];
|
||||
|
||||
// Only include if we have a grid source & return.
|
||||
if (hasReturn) {
|
||||
const card = {
|
||||
type: "energy-grid-neutrality-gauge",
|
||||
collection_key: collectionKey,
|
||||
};
|
||||
gaugeCards.push(card);
|
||||
}
|
||||
|
||||
// Only include if we have a solar source.
|
||||
if (hasSolar) {
|
||||
if (hasReturn) {
|
||||
const card = {
|
||||
type: "energy-solar-consumed-gauge",
|
||||
collection_key: collectionKey,
|
||||
};
|
||||
gaugeCards.push(card);
|
||||
}
|
||||
if (hasGrid) {
|
||||
const card = {
|
||||
type: "energy-self-sufficiency-gauge",
|
||||
collection_key: collectionKey,
|
||||
};
|
||||
gaugeCards.push(card);
|
||||
}
|
||||
}
|
||||
|
||||
// Only include if we have a grid
|
||||
if (hasGrid) {
|
||||
const card = {
|
||||
type: "energy-carbon-consumed-gauge",
|
||||
collection_key: collectionKey,
|
||||
};
|
||||
gaugeCards.push(card);
|
||||
}
|
||||
|
||||
if (gaugeCards.length) {
|
||||
view.sections!.push({
|
||||
type: "grid",
|
||||
column_span: 3,
|
||||
cards:
|
||||
gaugeCards.length === 1
|
||||
? [gaugeCards[0]]
|
||||
: gaugeCards.map((card) => ({
|
||||
...card,
|
||||
grid_options: { columns: 6 },
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
mainCards.push({
|
||||
type: "energy-compare",
|
||||
collection_key: collectionKey,
|
||||
grid_options: { columns: 36 },
|
||||
});
|
||||
|
||||
// Only include if we have a grid or battery.
|
||||
if (hasGrid || hasBattery) {
|
||||
view.cards!.push({
|
||||
mainCards.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_usage_graph_title"),
|
||||
type: "energy-usage-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: { columns: 36 },
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a solar source.
|
||||
if (hasSolar) {
|
||||
view.cards!.push({
|
||||
mainCards.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_solar_graph_title"),
|
||||
type: "energy-solar-graph",
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a grid or battery.
|
||||
if (hasGrid || hasBattery) {
|
||||
view.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_distribution_title"),
|
||||
type: "energy-distribution",
|
||||
view_layout: { position: "sidebar" },
|
||||
collection_key: collectionKey,
|
||||
grid_options: { columns: 36 },
|
||||
});
|
||||
}
|
||||
|
||||
if (hasGrid || hasSolar || hasBattery) {
|
||||
view.cards!.push({
|
||||
mainCards.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_sources_table_title"
|
||||
),
|
||||
type: "energy-sources-table",
|
||||
collection_key: collectionKey,
|
||||
types: ["grid", "solar", "battery"],
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a grid source & return.
|
||||
if (hasReturn) {
|
||||
view.cards!.push({
|
||||
type: "energy-grid-neutrality-gauge",
|
||||
view_layout: { position: "sidebar" },
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a solar source.
|
||||
if (hasSolar) {
|
||||
if (hasReturn) {
|
||||
view.cards!.push({
|
||||
type: "energy-solar-consumed-gauge",
|
||||
view_layout: { position: "sidebar" },
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
if (hasGrid) {
|
||||
view.cards!.push({
|
||||
type: "energy-self-sufficiency-gauge",
|
||||
view_layout: { position: "sidebar" },
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Only include if we have a grid
|
||||
if (hasGrid) {
|
||||
view.cards!.push({
|
||||
type: "energy-carbon-consumed-gauge",
|
||||
view_layout: { position: "sidebar" },
|
||||
collection_key: collectionKey,
|
||||
grid_options: { columns: 36 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,29 +158,38 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
hass,
|
||||
(d) => d.stat_consumption
|
||||
);
|
||||
view.cards!.push({
|
||||
mainCards.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_detail_graph_title"
|
||||
),
|
||||
type: "energy-devices-detail-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: { columns: 36 },
|
||||
});
|
||||
view.cards!.push({
|
||||
mainCards.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_graph_title"
|
||||
),
|
||||
type: "energy-devices-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: { columns: 36 },
|
||||
});
|
||||
view.cards!.push({
|
||||
mainCards.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_sankey_title"),
|
||||
type: "energy-sankey",
|
||||
collection_key: collectionKey,
|
||||
group_by_floor: showFloorsAndAreas,
|
||||
group_by_area: showFloorsAndAreas,
|
||||
grid_options: { columns: 36 },
|
||||
});
|
||||
}
|
||||
|
||||
view.sections!.push({
|
||||
type: "grid",
|
||||
column_span: 3,
|
||||
cards: mainCards,
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,21 @@ export class GasViewStrategy extends ReactiveElement {
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
sections: [{ type: "grid", cards: [] }],
|
||||
};
|
||||
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
max_columns: 3,
|
||||
sections: [{ type: "grid", cards: [], column_span: 3 }],
|
||||
footer: {
|
||||
card: {
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const energyCollection = getEnergyDataCollection(hass, {
|
||||
key: collectionKey,
|
||||
});
|
||||
@@ -49,6 +56,9 @@ export class GasViewStrategy extends ReactiveElement {
|
||||
title: hass.localize("ui.panel.energy.cards.energy_gas_graph_title"),
|
||||
type: "energy-gas-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: {
|
||||
columns: 24,
|
||||
},
|
||||
});
|
||||
|
||||
section.cards!.push({
|
||||
@@ -56,6 +66,9 @@ export class GasViewStrategy extends ReactiveElement {
|
||||
type: "energy-sources-table",
|
||||
collection_key: collectionKey,
|
||||
types: ["gas"],
|
||||
grid_options: {
|
||||
columns: 12,
|
||||
},
|
||||
});
|
||||
|
||||
return view;
|
||||
|
||||
@@ -14,14 +14,21 @@ export class WaterViewStrategy extends ReactiveElement {
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
sections: [{ type: "grid", cards: [] }],
|
||||
};
|
||||
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
max_columns: 3,
|
||||
sections: [{ type: "grid", cards: [], column_span: 3 }],
|
||||
footer: {
|
||||
card: {
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const energyCollection = getEnergyDataCollection(hass, {
|
||||
key: collectionKey,
|
||||
});
|
||||
@@ -52,6 +59,9 @@ export class WaterViewStrategy extends ReactiveElement {
|
||||
title: hass.localize("ui.panel.energy.cards.energy_water_graph_title"),
|
||||
type: "energy-water-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: {
|
||||
columns: 24,
|
||||
},
|
||||
});
|
||||
section.cards!.push({
|
||||
title: hass.localize(
|
||||
@@ -60,6 +70,9 @@ export class WaterViewStrategy extends ReactiveElement {
|
||||
type: "energy-sources-table",
|
||||
collection_key: collectionKey,
|
||||
types: ["water"],
|
||||
grid_options: {
|
||||
columns: 12,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,6 +89,9 @@ export class WaterViewStrategy extends ReactiveElement {
|
||||
collection_key: collectionKey,
|
||||
group_by_floor: showFloorsAndAreas,
|
||||
group_by_area: showFloorsAndAreas,
|
||||
grid_options: {
|
||||
columns: 24,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
SMALL_SCREEN_CONDITION,
|
||||
} from "../../lovelace/strategies/helpers/screen-conditions";
|
||||
import type { ToggleGroupCardConfig } from "../../lovelace/cards/types";
|
||||
import type { ButtonHeadingBadgeConfig } from "../../lovelace/heading-badges/types";
|
||||
|
||||
export interface LightViewStrategyConfig {
|
||||
type: "light";
|
||||
@@ -75,6 +76,7 @@ const processAreasForLight = (
|
||||
{
|
||||
type: "button",
|
||||
icon: "mdi:power",
|
||||
text: hass.localize("ui.panel.lovelace.strategy.light.off"),
|
||||
tap_action: {
|
||||
action: "perform-action",
|
||||
perform_action: "light.turn_on",
|
||||
@@ -89,11 +91,12 @@ const processAreasForLight = (
|
||||
conditions: [anyOnCondition],
|
||||
},
|
||||
],
|
||||
},
|
||||
} satisfies ButtonHeadingBadgeConfig,
|
||||
{
|
||||
type: "button",
|
||||
icon: "mdi:power",
|
||||
color: "amber",
|
||||
color: "orange",
|
||||
text: hass.localize("ui.panel.lovelace.strategy.light.on"),
|
||||
tap_action: {
|
||||
action: "perform-action",
|
||||
perform_action: "light.turn_off",
|
||||
@@ -102,7 +105,7 @@ const processAreasForLight = (
|
||||
},
|
||||
},
|
||||
visibility: [SMALL_SCREEN_CONDITION, anyOnCondition],
|
||||
},
|
||||
} satisfies ButtonHeadingBadgeConfig,
|
||||
] satisfies LovelaceCardConfig[],
|
||||
});
|
||||
|
||||
|
||||
@@ -49,6 +49,14 @@ class HuiClimateFanModesCardFeature
|
||||
|
||||
@state() _currentFanMode?: string;
|
||||
|
||||
private _renderFanModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._stateObj}
|
||||
attribute="fan_mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
@@ -175,14 +183,8 @@ class HuiClimateFanModesCardFeature
|
||||
.value=${this._currentFanMode}
|
||||
.disabled=${this._stateObj.state === UNAVAILABLE}
|
||||
@wa-select=${this._valueChanged}
|
||||
.options=${options.map((option) => ({
|
||||
...option,
|
||||
attributeIcon: {
|
||||
stateObj: stateObj,
|
||||
attribute: "fan_mode",
|
||||
attributeValue: option.value,
|
||||
},
|
||||
}))}
|
||||
.options=${options}
|
||||
.renderIcon=${this._renderFanModeIcon}
|
||||
><ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`;
|
||||
|
||||
@@ -48,6 +48,14 @@ class HuiClimatePresetModesCardFeature
|
||||
|
||||
@state() _currentPresetMode?: string;
|
||||
|
||||
private _renderPresetModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._stateObj}
|
||||
attribute="preset_mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
@@ -179,14 +187,8 @@ class HuiClimatePresetModesCardFeature
|
||||
.value=${this._currentPresetMode}
|
||||
.disabled=${this._stateObj.state === UNAVAILABLE}
|
||||
@wa-select=${this._valueChanged}
|
||||
.options=${options.map((option) => ({
|
||||
...option,
|
||||
attributeIcon: {
|
||||
stateObj: stateObj,
|
||||
attribute: "preset_mode",
|
||||
attributeValue: option.value,
|
||||
},
|
||||
}))}
|
||||
.options=${options}
|
||||
.renderIcon=${this._renderPresetModeIcon}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
|
||||
+10
-8
@@ -48,6 +48,14 @@ class HuiClimateSwingHorizontalModesCardFeature
|
||||
|
||||
@state() _currentSwingHorizontalMode?: string;
|
||||
|
||||
private _renderSwingHorizontalModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._stateObj}
|
||||
attribute="swing_horizontal_mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
@@ -187,14 +195,8 @@ class HuiClimateSwingHorizontalModesCardFeature
|
||||
.value=${this._currentSwingHorizontalMode}
|
||||
.disabled=${this._stateObj.state === UNAVAILABLE}
|
||||
@wa-select=${this._valueChanged}
|
||||
.options=${options.map((option) => ({
|
||||
...option,
|
||||
attributeIcon: {
|
||||
stateObj: stateObj,
|
||||
attribute: "swing_horizontal_mode",
|
||||
attributeValue: option.value,
|
||||
},
|
||||
}))}
|
||||
.options=${options}
|
||||
.renderIcon=${this._renderSwingHorizontalModeIcon}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiArrowOscillating}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
|
||||
@@ -48,6 +48,14 @@ class HuiClimateSwingModesCardFeature
|
||||
|
||||
@state() _currentSwingMode?: string;
|
||||
|
||||
private _renderSwingModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._stateObj}
|
||||
attribute="swing_mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
@@ -179,14 +187,8 @@ class HuiClimateSwingModesCardFeature
|
||||
.value=${this._currentSwingMode}
|
||||
.disabled=${this._stateObj.state === UNAVAILABLE}
|
||||
@wa-select=${this._valueChanged}
|
||||
.options=${options.map((option) => ({
|
||||
...option,
|
||||
attributeIcon: {
|
||||
stateObj,
|
||||
attribute: "swing_mode",
|
||||
attributeValue: option.value,
|
||||
},
|
||||
}))}
|
||||
.options=${options}
|
||||
.renderIcon=${this._renderSwingModeIcon}
|
||||
><ha-svg-icon slot="icon" .path=${mdiArrowOscillating}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`;
|
||||
|
||||
@@ -47,6 +47,14 @@ class HuiFanPresetModesCardFeature
|
||||
|
||||
@state() _currentPresetMode?: string;
|
||||
|
||||
private _renderPresetModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._stateObj}
|
||||
attribute="preset_mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
@@ -173,14 +181,8 @@ class HuiFanPresetModesCardFeature
|
||||
.value=${this._currentPresetMode}
|
||||
.disabled=${this._stateObj.state === UNAVAILABLE}
|
||||
@wa-select=${this._valueChanged}
|
||||
.options=${options.map((option) => ({
|
||||
...option,
|
||||
attributeIcon: {
|
||||
stateObj: stateObj,
|
||||
attribute: "preset_mode",
|
||||
attributeValue: option.value,
|
||||
},
|
||||
}))}
|
||||
.options=${options}
|
||||
.renderIcon=${this._renderPresetModeIcon}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
|
||||
@@ -48,6 +48,14 @@ class HuiHumidifierModesCardFeature
|
||||
|
||||
@state() _currentMode?: string;
|
||||
|
||||
private _renderModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._stateObj}
|
||||
attribute="mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
@@ -174,14 +182,8 @@ class HuiHumidifierModesCardFeature
|
||||
.value=${this._currentMode}
|
||||
.disabled=${this._stateObj.state === UNAVAILABLE}
|
||||
@wa-select=${this._valueChanged}
|
||||
.options=${options.map((option) => ({
|
||||
...option,
|
||||
attributeIcon: {
|
||||
stateObj,
|
||||
attribute: "mode",
|
||||
attributeValue: option.value,
|
||||
},
|
||||
}))}
|
||||
.options=${options}
|
||||
.renderIcon=${this._renderModeIcon}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
|
||||
@@ -49,6 +49,14 @@ class HuiWaterHeaterOperationModeCardFeature
|
||||
|
||||
@state() _currentOperationMode?: OperationMode;
|
||||
|
||||
private _renderOperationModeIcon = (value: string) =>
|
||||
html`<ha-attribute-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._stateObj}
|
||||
attribute="operation_mode"
|
||||
.attributeValue=${value}
|
||||
></ha-attribute-icon>`;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
@@ -153,14 +161,8 @@ class HuiWaterHeaterOperationModeCardFeature
|
||||
.value=${this._currentOperationMode}
|
||||
.disabled=${this._stateObj.state === UNAVAILABLE}
|
||||
@wa-select=${this._valueChanged}
|
||||
.options=${options.map((option) => ({
|
||||
...option,
|
||||
attributeIcon: {
|
||||
stateObj: this._stateObj,
|
||||
attribute: "operation_mode",
|
||||
attributeValue: option.value,
|
||||
},
|
||||
}))}
|
||||
.options=${options}
|
||||
.renderIcon=${this._renderOperationModeIcon}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
|
||||
@@ -218,7 +218,9 @@ function formatTooltip(
|
||||
}
|
||||
// when comparing the first value is offset to match the main period
|
||||
// and the real date is in the third value
|
||||
const date = new Date(params[0].value?.[2] ?? params[0].value?.[0]);
|
||||
// find the first param with the real date to handle gap-filled entries
|
||||
const origDate = params.find((p) => p.value?.[2] != null)?.value?.[2];
|
||||
const date = new Date(origDate ?? params[0].value?.[0]);
|
||||
let period: string;
|
||||
|
||||
if (suggestedPeriod === "month") {
|
||||
|
||||
@@ -610,18 +610,21 @@ class HuiPowerSankeyCard
|
||||
});
|
||||
|
||||
// Collect battery power (positive = discharge, negative = charge)
|
||||
// Sum all battery values first, then determine net direction.
|
||||
// Momentary power should only flow in one direction across all batteries.
|
||||
let net_battery = 0;
|
||||
prefs.energy_sources
|
||||
.filter((source) => source.type === "battery")
|
||||
.forEach((source) => {
|
||||
if (source.type === "battery" && source.stat_rate) {
|
||||
const value = this._getCurrentPower(source.stat_rate);
|
||||
if (value > 0) {
|
||||
from_battery += value;
|
||||
} else if (value < 0) {
|
||||
to_battery += Math.abs(value);
|
||||
}
|
||||
net_battery += this._getCurrentPower(source.stat_rate);
|
||||
}
|
||||
});
|
||||
if (net_battery > 0) {
|
||||
from_battery = net_battery;
|
||||
} else if (net_battery < 0) {
|
||||
to_battery = Math.abs(net_battery);
|
||||
}
|
||||
|
||||
// Calculate total consumption
|
||||
const used_total = from_grid + solar + from_battery - to_grid - to_battery;
|
||||
|
||||
@@ -131,9 +131,10 @@ export class HuiDiscoveredDevicesCard
|
||||
}
|
||||
|
||||
// Update visibility based on admin status and discovered devices count
|
||||
const shouldBeHidden =
|
||||
const shouldBeHidden = Boolean(
|
||||
!this.hass.user?.is_admin ||
|
||||
(this._config.hide_empty && this._discoveredFlows.length === 0);
|
||||
(this._config.hide_empty && this._discoveredFlows.length === 0)
|
||||
);
|
||||
|
||||
if (shouldBeHidden !== this.hidden) {
|
||||
this.style.display = shouldBeHidden ? "none" : "";
|
||||
|
||||
@@ -68,9 +68,9 @@ export class HuiDistributionCard
|
||||
|
||||
// Strategy 1: Try to find power sensors (W, kW) - most common use case
|
||||
const powerFilter = (stateObj: HassEntity): boolean => {
|
||||
const unit = stateObj.attributes.unit_of_measurement;
|
||||
const stateValue = Number(stateObj.state);
|
||||
return (unit === "W" || unit === "kW") && !isNaN(stateValue);
|
||||
const deviceClass = stateObj.attributes.device_class;
|
||||
return deviceClass === "power" && !isNaN(stateValue);
|
||||
};
|
||||
|
||||
let foundEntities = findEntities(
|
||||
|
||||
@@ -97,9 +97,10 @@ export class HuiRepairsCard
|
||||
}
|
||||
|
||||
// Update visibility based on admin status and repairs count
|
||||
const shouldBeHidden =
|
||||
const shouldBeHidden = Boolean(
|
||||
!this.hass.user?.is_admin ||
|
||||
(this._config.hide_empty && this._repairsIssues.length === 0);
|
||||
(this._config.hide_empty && this._repairsIssues.length === 0)
|
||||
);
|
||||
|
||||
if (shouldBeHidden !== this.hidden) {
|
||||
this.style.display = shouldBeHidden ? "none" : "";
|
||||
|
||||
@@ -91,9 +91,10 @@ export class HuiUpdatesCard extends LitElement implements LovelaceCard {
|
||||
const updateEntities = this._getUpdateEntities();
|
||||
|
||||
// Update visibility based on admin status and updates count
|
||||
const shouldBeHidden =
|
||||
const shouldBeHidden = Boolean(
|
||||
!this.hass.user?.is_admin ||
|
||||
(this._config.hide_empty && updateEntities.length === 0);
|
||||
(this._config.hide_empty && updateEntities.length === 0)
|
||||
);
|
||||
|
||||
if (shouldBeHidden !== this.hidden) {
|
||||
this.style.display = shouldBeHidden ? "none" : "";
|
||||
@@ -103,7 +104,7 @@ export class HuiUpdatesCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._config || !this.hass || this.hidden) {
|
||||
if (!this._config || !this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _measure() {
|
||||
this.narrow = this.offsetWidth < 450;
|
||||
this.narrow = this.offsetWidth < 425;
|
||||
this._collapseButtons = this.offsetWidth < 320;
|
||||
}
|
||||
|
||||
|
||||
@@ -226,6 +226,7 @@ export class HuiDialogEditBadge
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.value=${this._badgeConfig}
|
||||
in-dialog
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
@editor-save=${this._save}
|
||||
@@ -314,7 +315,9 @@ export class HuiDialogEditBadge
|
||||
}
|
||||
|
||||
private _toggleMode(): void {
|
||||
this._badgeEditorEl?.toggleMode();
|
||||
withViewTransition(() => {
|
||||
this._badgeEditorEl?.toggleMode();
|
||||
});
|
||||
}
|
||||
|
||||
private _opened() {
|
||||
|
||||
@@ -97,6 +97,7 @@ export class HuiDialogSuggestBadge extends LitElement {
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._badgeConfig}
|
||||
in-dialog
|
||||
></ha-yaml-editor>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -203,6 +203,7 @@ export class HuiDialogEditCard
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.value=${this._cardConfig}
|
||||
in-dialog
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
@editor-save=${this._save}
|
||||
@@ -297,7 +298,9 @@ export class HuiDialogEditCard
|
||||
}
|
||||
|
||||
private _toggleMode(): void {
|
||||
this._cardEditorEl?.toggleMode();
|
||||
withViewTransition(() => {
|
||||
this._cardEditorEl?.toggleMode();
|
||||
});
|
||||
}
|
||||
|
||||
private _opened() {
|
||||
|
||||
@@ -133,6 +133,7 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._cardConfig}
|
||||
in-dialog
|
||||
></ha-yaml-editor>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -57,6 +57,9 @@ export abstract class HuiElementEditor<
|
||||
|
||||
@property({ attribute: false }) public context?: C;
|
||||
|
||||
@property({ type: Boolean, attribute: "in-dialog" })
|
||||
public inDialog = false;
|
||||
|
||||
@state() private _config?: T;
|
||||
|
||||
@state() private _configElement?: LovelaceGenericElementEditor;
|
||||
@@ -150,6 +153,9 @@ export abstract class HuiElementEditor<
|
||||
}
|
||||
|
||||
public toggleMode() {
|
||||
if (!this.GUImode) {
|
||||
this._yamlEditor?.disableCodeEditorFullscreen();
|
||||
}
|
||||
this.GUImode = !this.GUImode;
|
||||
}
|
||||
|
||||
@@ -243,6 +249,7 @@ export abstract class HuiElementEditor<
|
||||
.defaultValue=${this._config}
|
||||
autofocus
|
||||
.hass=${this.hass}
|
||||
.inDialog=${this.inDialog}
|
||||
@value-changed=${this._handleYAMLChanged}
|
||||
@blur=${this._onBlurYaml}
|
||||
@keydown=${this._ignoreKeydown}
|
||||
|
||||
@@ -127,6 +127,7 @@ export class HuiDialogEditSection
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
autofocus
|
||||
in-dialog
|
||||
@value-changed=${this._viewYamlChanged}
|
||||
></ha-yaml-editor>
|
||||
`;
|
||||
|
||||
@@ -162,6 +162,7 @@ export class HuiDialogEditView extends LitElement {
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
autofocus
|
||||
in-dialog
|
||||
@value-changed=${this._viewYamlChanged}
|
||||
></ha-yaml-editor>
|
||||
`;
|
||||
|
||||
@@ -91,7 +91,6 @@ export class HuiDialogEditViewFooter extends LitElement {
|
||||
<hui-view-footer-settings-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
.maxColumns=${this._params.maxColumns}
|
||||
@config-changed=${this._configChanged}
|
||||
></hui-view-footer-settings-editor>
|
||||
`;
|
||||
@@ -106,7 +105,7 @@ export class HuiDialogEditViewFooter extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${title}
|
||||
.width=${this._yamlMode ? "full" : "large"}
|
||||
width="medium"
|
||||
@closed=${this._dialogClosed}
|
||||
class=${this._yamlMode ? "yaml-mode" : ""}
|
||||
>
|
||||
|
||||
@@ -1,53 +1,48 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { LovelaceViewFooterConfig } from "../../../../data/lovelace/config/view";
|
||||
import {
|
||||
DEFAULT_FOOTER_MAX_WIDTH_PX,
|
||||
type LovelaceViewFooterConfig,
|
||||
} from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "max_width",
|
||||
selector: {
|
||||
number: {
|
||||
min: 100,
|
||||
max: 1600,
|
||||
step: 10,
|
||||
unit_of_measurement: "px",
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies HaFormSchema[];
|
||||
|
||||
@customElement("hui-view-footer-settings-editor")
|
||||
export class HuiViewFooterSettingsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceViewFooterConfig;
|
||||
|
||||
@property({ attribute: false }) public maxColumns = 4;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(maxColumns: number) =>
|
||||
[
|
||||
{
|
||||
name: "column_span",
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: maxColumns,
|
||||
slider_ticks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const data = {
|
||||
column_span: this.config?.column_span || 1,
|
||||
max_width: this.config?.max_width || DEFAULT_FOOTER_MAX_WIDTH_PX,
|
||||
};
|
||||
|
||||
const schema = this._schema(this.maxColumns);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.schema=${SCHEMA}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
@@ -65,19 +60,10 @@ export class HuiViewFooterSettingsEditor extends LitElement {
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view_footer.settings.${schema.name}`
|
||||
);
|
||||
|
||||
private _computeHelper = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view_footer.settings.${schema.name}_helper`
|
||||
) || "";
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { LovelaceViewFooterConfig } from "../../../../data/lovelace/config/
|
||||
export interface EditViewFooterDialogParams {
|
||||
saveConfig: (config: LovelaceViewFooterConfig) => void;
|
||||
config: LovelaceViewFooterConfig;
|
||||
maxColumns: number;
|
||||
}
|
||||
|
||||
export const showEditViewFooterDialog = (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user