mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-12 10:49:25 +00:00
Compare commits
74 Commits
20240605.0
...
dashboard_
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ebe5207b6e | ||
![]() |
bd1ede4145 | ||
![]() |
321a085c0e | ||
![]() |
6a3041988a | ||
![]() |
23fcdf876c | ||
![]() |
00f325e961 | ||
![]() |
d00b3cfc61 | ||
![]() |
4cc9e74ea8 | ||
![]() |
a56b9a96ce | ||
![]() |
d4b5f4bc14 | ||
![]() |
cf1523ee73 | ||
![]() |
f5d571ca84 | ||
![]() |
362e92f313 | ||
![]() |
5ddf72b973 | ||
![]() |
6e78c28f51 | ||
![]() |
772f0bb669 | ||
![]() |
846c2a848f | ||
![]() |
8495757005 | ||
![]() |
9960d38b91 | ||
![]() |
d3222f8bb0 | ||
![]() |
2e5cce5409 | ||
![]() |
f78946447f | ||
![]() |
eb0579ddc5 | ||
![]() |
686424fc70 | ||
![]() |
039e9b40bd | ||
![]() |
8272bef890 | ||
![]() |
62528b2413 | ||
![]() |
fa24f529e0 | ||
![]() |
43a54f6cda | ||
![]() |
9c153bbd58 | ||
![]() |
27afe9ecb7 | ||
![]() |
72f989e2bd | ||
![]() |
a6ef46565f | ||
![]() |
a35ac09688 | ||
![]() |
27024135ea | ||
![]() |
8759ed740a | ||
![]() |
bb3e8ae33d | ||
![]() |
b5b60c9bf0 | ||
![]() |
3b6a2cf7d8 | ||
![]() |
7e10e14102 | ||
![]() |
a580abab4a | ||
![]() |
11523c08c4 | ||
![]() |
7a8988528b | ||
![]() |
2a6380f083 | ||
![]() |
29881c8bb4 | ||
![]() |
56254ddf03 | ||
![]() |
007ba70641 | ||
![]() |
3e1227b064 | ||
![]() |
067e179f26 | ||
![]() |
9a3f7df25e | ||
![]() |
c7b4e8f37c | ||
![]() |
bfa8b886ab | ||
![]() |
433c00b73a | ||
![]() |
a497f42f73 | ||
![]() |
165723cb5b | ||
![]() |
42b5fa696a | ||
![]() |
59062d96a8 | ||
![]() |
d36bbfe07d | ||
![]() |
0d489213a4 | ||
![]() |
c54acc9369 | ||
![]() |
562bc084f0 | ||
![]() |
6fce2f35a5 | ||
![]() |
f4e24bed2e | ||
![]() |
09969c0e2d | ||
![]() |
4b0181774b | ||
![]() |
272db5e9e8 | ||
![]() |
9ae3a824d9 | ||
![]() |
9db55c9391 | ||
![]() |
59697127c0 | ||
![]() |
565600e945 | ||
![]() |
721eebf367 | ||
![]() |
f5ae842167 | ||
![]() |
3575734ed0 | ||
![]() |
4e8de1f64d |
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
|
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
|
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
script/release
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@v2.0.5
|
||||
uses: softprops/action-gh-release@v2.0.6
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.2.2.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.3.1.cjs
|
||||
|
@@ -92,8 +92,8 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
useBuiltIns: latestBuild ? false : "usage",
|
||||
corejs: latestBuild ? false : dependencies["core-js"],
|
||||
useBuiltIns: "usage",
|
||||
corejs: dependencies["core-js"],
|
||||
bugfixes: true,
|
||||
shippedProposals: true,
|
||||
},
|
||||
@@ -157,7 +157,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
exclude: [
|
||||
path.join(paths.polymer_dir, "src/resources/polyfills"),
|
||||
...[
|
||||
"@formatjs/intl-\\w+",
|
||||
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
|
||||
"@lit-labs/virtualizer/polyfills",
|
||||
"@webcomponents/scoped-custom-element-registry",
|
||||
"element-internals-polyfill",
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list";
|
||||
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { Auth, Connection } from "home-assistant-js-websocket";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -28,6 +27,7 @@ import { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
|
||||
import "../../../../src/layouts/hass-loading-screen";
|
||||
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
|
||||
import "./hc-layout";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
|
||||
@customElement("hc-cast")
|
||||
class HcCast extends LitElement {
|
||||
@@ -83,37 +83,37 @@ class HcCast extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<div class="section-header">PICK A VIEW</div>
|
||||
<paper-listbox
|
||||
attr-for-selected="data-path"
|
||||
.selected=${this.castManager.status.lovelacePath || ""}
|
||||
>
|
||||
<mwc-list @action=${this._handlePickView} activatable>
|
||||
${(
|
||||
this.lovelaceViews ?? [
|
||||
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
|
||||
]
|
||||
).map(
|
||||
(view, idx) => html`
|
||||
<paper-icon-item
|
||||
@click=${this._handlePickView}
|
||||
data-path=${view.path || idx}
|
||||
(view, idx) =>
|
||||
html`<ha-list-item
|
||||
graphic="avatar"
|
||||
.activated=${this.castManager.status?.lovelacePath ===
|
||||
(view.path ?? idx)}
|
||||
.selected=${this.castManager.status?.lovelacePath ===
|
||||
(view.path ?? idx)}
|
||||
>
|
||||
${view.title || view.path || "Unnamed view"}
|
||||
${view.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
.icon=${view.icon}
|
||||
slot="item-icon"
|
||||
slot="graphic"
|
||||
></ha-icon>
|
||||
`
|
||||
: html`<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${mdiViewDashboard}
|
||||
></ha-svg-icon>`}
|
||||
${view.title || view.path || "Unnamed view"}
|
||||
</paper-icon-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
></ha-svg-icon>`}</ha-list-item
|
||||
> `
|
||||
)}</mwc-list
|
||||
>
|
||||
`}
|
||||
|
||||
<div class="card-actions">
|
||||
${this.castManager.status
|
||||
? html`
|
||||
@@ -185,8 +185,8 @@ class HcCast extends LitElement {
|
||||
this.castManager.requestSession();
|
||||
}
|
||||
|
||||
private async _handlePickView(ev: Event) {
|
||||
const path = (ev.currentTarget as any).getAttribute("data-path");
|
||||
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
|
||||
const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index;
|
||||
await ensureConnectedCastSession(this.castManager!, this.auth!);
|
||||
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
|
||||
}
|
||||
@@ -249,26 +249,14 @@ class HcCast extends LitElement {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
paper-listbox {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
paper-listbox ha-icon,
|
||||
paper-listbox ha-svg-icon {
|
||||
ha-list-item ha-icon,
|
||||
ha-list-item ha-svg-icon {
|
||||
padding: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
paper-icon-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
paper-icon-item[disabled] {
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
:host([hide-icons]) paper-icon-item {
|
||||
--paper-item-icon-width: 0px;
|
||||
:host([hide-icons]) ha-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-circular-progress";
|
||||
import { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
demoConfigs,
|
||||
selectedDemoConfig,
|
||||
selectedDemoConfigIndex,
|
||||
setDemoConfig,
|
||||
} from "../configs/demo-configs";
|
||||
|
||||
@customElement("ha-demo-card")
|
||||
@@ -64,9 +64,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<mwc-button @click=${this._nextConfig} .disabled=${this._switching}>
|
||||
<ha-button @click=${this._nextConfig} .disabled=${this._switching}>
|
||||
${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")}
|
||||
</mwc-button>
|
||||
</ha-button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="small-hidden">
|
||||
@@ -87,9 +87,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
</div>
|
||||
<div class="actions small-hidden">
|
||||
<a href="https://www.home-assistant.io" target="_blank">
|
||||
<mwc-button>
|
||||
<ha-button>
|
||||
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
|
||||
</mwc-button>
|
||||
</ha-button>
|
||||
</a>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -113,13 +113,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
|
||||
private async _updateConfig(index: number) {
|
||||
this._switching = true;
|
||||
try {
|
||||
await setDemoConfig(this.hass, this.lovelace!, index);
|
||||
} catch (err: any) {
|
||||
alert("Failed to switch config :-(");
|
||||
} finally {
|
||||
this._switching = false;
|
||||
}
|
||||
fireEvent(this, "set-demo-config" as any, { index });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -149,7 +143,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.picker mwc-button {
|
||||
.picker ha-button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
@@ -1,9 +1,12 @@
|
||||
import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
import { selectedDemoConfig } from "../configs/demo-configs";
|
||||
import {
|
||||
selectedDemoConfig,
|
||||
selectedDemoConfigIndex,
|
||||
setDemoConfig,
|
||||
} from "../configs/demo-configs";
|
||||
import "../custom-cards/cast-demo-row";
|
||||
import "../custom-cards/ha-demo-card";
|
||||
import type { HADemoCard } from "../custom-cards/ha-demo-card";
|
||||
|
||||
export const mockLovelace = (
|
||||
hass: MockHomeAssistant,
|
||||
@@ -19,17 +22,22 @@ export const mockLovelace = (
|
||||
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
||||
};
|
||||
|
||||
customElements.whenDefined("hui-card").then(() => {
|
||||
customElements.whenDefined("hui-root").then(() => {
|
||||
// eslint-disable-next-line
|
||||
const HUIView = customElements.get("hui-card");
|
||||
// Patch HUI-VIEW to make the lovelace object available to the demo card
|
||||
const oldCreateCard = HUIView!.prototype.createElement;
|
||||
const HUIRoot = customElements.get("hui-root")!;
|
||||
|
||||
HUIView!.prototype.createElement = function (config) {
|
||||
const el = oldCreateCard.call(this, config);
|
||||
if (config.type === "custom:ha-demo-card") {
|
||||
(el as HADemoCard).lovelace = this.lovelace;
|
||||
}
|
||||
return el;
|
||||
const oldFirstUpdated = HUIRoot.prototype.firstUpdated;
|
||||
|
||||
HUIRoot.prototype.firstUpdated = function (changedProperties) {
|
||||
oldFirstUpdated.call(this, changedProperties);
|
||||
this.addEventListener("set-demo-config", async (ev) => {
|
||||
const index = (ev as CustomEvent).detail.index;
|
||||
try {
|
||||
await setDemoConfig(this.hass, this.lovelace!, index);
|
||||
} catch (err: any) {
|
||||
setDemoConfig(this.hass, this.lovelace!, selectedDemoConfigIndex);
|
||||
alert("Failed to switch config :-(");
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { load } from "js-yaml";
|
||||
import { html, css, LitElement, PropertyValues } from "lit";
|
||||
import { LitElement, PropertyValueMap, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../src/panels/lovelace/cards/hui-card";
|
||||
import type { HuiCard } from "../../../src/panels/lovelace/cards/hui-card";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
|
||||
export interface DemoCardConfig {
|
||||
@@ -19,7 +21,12 @@ class DemoCard extends LitElement {
|
||||
|
||||
@state() private _size?: number;
|
||||
|
||||
@query("#card") private _card!: HTMLElement;
|
||||
@query("hui-card", false) private _card?: HuiCard;
|
||||
|
||||
private _config = memoizeOne((config: string) => {
|
||||
const c = (load(config) as any)[0];
|
||||
return c;
|
||||
});
|
||||
|
||||
render() {
|
||||
return html`
|
||||
@@ -30,63 +37,32 @@ class DemoCard extends LitElement {
|
||||
: ""}
|
||||
</h2>
|
||||
<div class="root">
|
||||
<div id="card"></div>
|
||||
${this.showConfig ? html`<pre>${this.config.config.trim()}</pre>` : ""}
|
||||
<hui-card
|
||||
.config=${this._config(this.config.config)}
|
||||
.hass=${this.hass}
|
||||
@card-updated=${this._cardUpdated}
|
||||
></hui-card>
|
||||
${this.showConfig
|
||||
? html`<pre>${this.config.config.trim()}</pre>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("config")) {
|
||||
const card = this._card;
|
||||
while (card.lastChild) {
|
||||
card.removeChild(card.lastChild);
|
||||
}
|
||||
|
||||
const el = this._createCardElement((load(this.config.config) as any)[0]);
|
||||
card.appendChild(el);
|
||||
this._getSize(el);
|
||||
}
|
||||
|
||||
if (changedProps.has("hass")) {
|
||||
const card = this._card.lastChild;
|
||||
if (card) {
|
||||
(card as any).hass = this.hass;
|
||||
}
|
||||
}
|
||||
private async _cardUpdated(ev) {
|
||||
ev.stopPropagation();
|
||||
this._updateSize();
|
||||
}
|
||||
|
||||
async _getSize(el) {
|
||||
await customElements.whenDefined(el.localName);
|
||||
|
||||
if (!("getCardSize" in el)) {
|
||||
this._size = undefined;
|
||||
return;
|
||||
}
|
||||
this._size = await el.getCardSize();
|
||||
private async _updateSize() {
|
||||
this._size = await this._card?.getCardSize();
|
||||
}
|
||||
|
||||
_createCardElement(cardConfig) {
|
||||
const element = createCardElement(cardConfig);
|
||||
if (this.hass) {
|
||||
element.hass = this.hass;
|
||||
}
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev) => {
|
||||
ev.stopPropagation();
|
||||
this._rebuildCard(element, cardConfig);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
return element;
|
||||
}
|
||||
|
||||
_rebuildCard(cardElToReplace, config) {
|
||||
const newCardEl = this._createCardElement(config);
|
||||
cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace);
|
||||
protected update(
|
||||
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
||||
): void {
|
||||
super.update(_changedProperties);
|
||||
this._updateSize();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
@@ -101,7 +77,7 @@ class DemoCard extends LitElement {
|
||||
font-size: 0.5em;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
#card {
|
||||
hui-card {
|
||||
max-width: 400px;
|
||||
width: 100vw;
|
||||
}
|
||||
|
67
package.json
67
package.json
@@ -25,15 +25,15 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.24.6",
|
||||
"@braintree/sanitize-url": "7.0.2",
|
||||
"@codemirror/autocomplete": "6.16.2",
|
||||
"@codemirror/commands": "6.5.0",
|
||||
"@codemirror/language": "6.10.1",
|
||||
"@babel/runtime": "7.24.7",
|
||||
"@braintree/sanitize-url": "7.0.3",
|
||||
"@codemirror/autocomplete": "6.16.3",
|
||||
"@codemirror/commands": "6.6.0",
|
||||
"@codemirror/language": "6.10.2",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.26.3",
|
||||
"@codemirror/view": "6.28.2",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.12.5",
|
||||
"@formatjs/intl-displaynames": "6.6.8",
|
||||
@@ -43,17 +43,17 @@
|
||||
"@formatjs/intl-numberformat": "8.10.3",
|
||||
"@formatjs/intl-pluralrules": "5.2.14",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.14",
|
||||
"@fullcalendar/core": "6.1.13",
|
||||
"@fullcalendar/daygrid": "6.1.13",
|
||||
"@fullcalendar/interaction": "6.1.13",
|
||||
"@fullcalendar/list": "6.1.13",
|
||||
"@fullcalendar/luxon3": "6.1.13",
|
||||
"@fullcalendar/timegrid": "6.1.13",
|
||||
"@fullcalendar/core": "6.1.11",
|
||||
"@fullcalendar/daygrid": "6.1.11",
|
||||
"@fullcalendar/interaction": "6.1.11",
|
||||
"@fullcalendar/list": "6.1.11",
|
||||
"@fullcalendar/luxon3": "6.1.11",
|
||||
"@fullcalendar/timegrid": "6.1.11",
|
||||
"@lezer/highlight": "1.2.0",
|
||||
"@lit-labs/context": "0.4.1",
|
||||
"@lit-labs/motion": "1.0.7",
|
||||
"@lit-labs/observers": "2.0.2",
|
||||
"@lit-labs/virtualizer": "2.0.12",
|
||||
"@lit-labs/virtualizer": "2.0.13",
|
||||
"@lrnwebcomponents/simple-tooltip": "8.0.2",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
@@ -88,8 +88,8 @@
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.3.13",
|
||||
"@vaadin/vaadin-themable-mixin": "24.3.13",
|
||||
"@vaadin/combo-box": "24.4.0",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.0",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -110,7 +110,7 @@
|
||||
"fuse.js": "7.0.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.3.0",
|
||||
"home-assistant-js-websocket": "9.4.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.5.14",
|
||||
"js-yaml": "4.1.0",
|
||||
@@ -149,26 +149,26 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.6",
|
||||
"@babel/core": "7.24.7",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.2",
|
||||
"@babel/plugin-proposal-decorators": "7.24.6",
|
||||
"@babel/plugin-transform-runtime": "7.24.6",
|
||||
"@babel/preset-env": "7.24.6",
|
||||
"@babel/preset-typescript": "7.24.6",
|
||||
"@babel/plugin-proposal-decorators": "7.24.7",
|
||||
"@babel/plugin-transform-runtime": "7.24.7",
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.13.2",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.5.0",
|
||||
"@octokit/auth-oauth-device": "7.1.1",
|
||||
"@octokit/plugin-retry": "7.1.1",
|
||||
"@octokit/rest": "20.1.1",
|
||||
"@octokit/rest": "21.0.0",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rollup/plugin-babel": "6.0.4",
|
||||
"@rollup/plugin-commonjs": "25.0.8",
|
||||
"@rollup/plugin-commonjs": "26.0.1",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-replace": "5.0.5",
|
||||
"@rollup/plugin-replace": "5.0.7",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.14",
|
||||
"@types/chromecast-caf-receiver": "6.0.15",
|
||||
"@types/chromecast-caf-sender": "1.0.10",
|
||||
"@types/color-name": "1.1.4",
|
||||
"@types/glob": "8.1.0",
|
||||
@@ -185,8 +185,8 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "7.11.0",
|
||||
"@typescript-eslint/parser": "7.11.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.13.1",
|
||||
"@typescript-eslint/parser": "7.13.1",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
@@ -205,7 +205,7 @@
|
||||
"eslint-plugin-wc": "2.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "10.4.1",
|
||||
"glob": "10.4.2",
|
||||
"gulp": "5.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.0.0",
|
||||
@@ -214,7 +214,7 @@
|
||||
"husky": "9.0.11",
|
||||
"instant-mocha": "1.5.2",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.2.5",
|
||||
"lint-staged": "15.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
@@ -224,7 +224,7 @@
|
||||
"object-hash": "3.0.0",
|
||||
"open": "10.1.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.3.0",
|
||||
"prettier": "3.3.2",
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
@@ -233,12 +233,12 @@
|
||||
"sinon": "18.0.0",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.15.1",
|
||||
"tar": "7.2.0",
|
||||
"tar": "7.4.0",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "5.91.0",
|
||||
"webpack": "5.92.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
@@ -253,8 +253,9 @@
|
||||
"lit": "2.8.0",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.11",
|
||||
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
|
||||
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.2.2"
|
||||
"packageManager": "yarn@4.3.1"
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240605.0"
|
||||
version = "20240610.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -31,6 +31,7 @@ import {
|
||||
mdiFormatListBulleted,
|
||||
mdiFormatListCheckbox,
|
||||
mdiFormTextbox,
|
||||
mdiForumOutline,
|
||||
mdiGauge,
|
||||
mdiGoogleAssistant,
|
||||
mdiGoogleCirclesCommunities,
|
||||
@@ -98,7 +99,7 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
calendar: mdiCalendar,
|
||||
climate: mdiThermostat,
|
||||
configurator: mdiCog,
|
||||
conversation: mdiMicrophoneMessage,
|
||||
conversation: mdiForumOutline,
|
||||
counter: mdiCounter,
|
||||
date: mdiCalendar,
|
||||
datetime: mdiCalendarClock,
|
||||
@@ -235,6 +236,8 @@ export const SENSOR_ENTITIES = [
|
||||
"weather",
|
||||
];
|
||||
|
||||
export const ASSIST_ENTITIES = ["conversation", "stt", "tts"];
|
||||
|
||||
/** Domains that render an input element instead of a text value when displayed in a row.
|
||||
* Those rows should then not show a cursor pointer when hovered (which would normally
|
||||
* be the default) unless the element itself enforces it (e.g. a button). Also those elements
|
||||
|
1
src/common/dom/prevent_default.ts
Normal file
1
src/common/dom/prevent_default.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const preventDefault = (ev) => ev.preventDefault();
|
@@ -47,6 +47,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
@property({ type: Boolean }) public readOnly = false;
|
||||
|
||||
@property({ type: Boolean }) public linewrap = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "autocomplete-entities" })
|
||||
public autocompleteEntities = false;
|
||||
|
||||
@@ -134,6 +136,13 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
),
|
||||
});
|
||||
}
|
||||
if (changedProps.has("linewrap")) {
|
||||
transactions.push({
|
||||
effects: this._loadedCodeMirror!.linewrapCompartment!.reconfigure(
|
||||
this.linewrap ? this._loadedCodeMirror!.EditorView.lineWrapping : []
|
||||
),
|
||||
});
|
||||
}
|
||||
if (changedProps.has("_value") && this._value !== this.value) {
|
||||
transactions.push({
|
||||
changes: {
|
||||
@@ -181,6 +190,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this._loadedCodeMirror.readonlyCompartment.of(
|
||||
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
|
||||
),
|
||||
this._loadedCodeMirror.linewrapCompartment.of(
|
||||
this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
|
||||
),
|
||||
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
|
||||
];
|
||||
|
||||
|
@@ -89,13 +89,18 @@ export class HaFilterDomains extends LitElement {
|
||||
});
|
||||
|
||||
return Array.from(domains.values())
|
||||
.map((domain) => ({
|
||||
domain,
|
||||
name: domainToName(this.hass.localize, domain),
|
||||
}))
|
||||
.filter(
|
||||
(entry) =>
|
||||
!filter ||
|
||||
entry.toLowerCase().includes(filter) ||
|
||||
domainToName(this.hass.localize, entry).toLowerCase().includes(filter)
|
||||
entry.domain.toLowerCase().includes(filter) ||
|
||||
entry.name.toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) => stringCompare(a, b, this.hass.locale.language));
|
||||
.sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language))
|
||||
.map((entry) => entry.domain);
|
||||
});
|
||||
|
||||
protected updated(changed) {
|
||||
|
@@ -2,7 +2,7 @@ import type { Selector } from "../../data/selector";
|
||||
import type { HaFormSchema } from "./types";
|
||||
|
||||
export const computeInitialHaFormData = (
|
||||
schema: HaFormSchema[]
|
||||
schema: HaFormSchema[] | readonly HaFormSchema[]
|
||||
): Record<string, any> => {
|
||||
const data = {};
|
||||
schema.forEach((field) => {
|
||||
@@ -36,6 +36,8 @@ export const computeInitialHaFormData = (
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
};
|
||||
} else if (field.type === "expandable") {
|
||||
data[field.name] = computeInitialHaFormData(field.schema);
|
||||
} else if ("selector" in field) {
|
||||
const selector: Selector = field.selector;
|
||||
|
||||
|
233
src/components/ha-grid-size-picker.ts
Normal file
233
src/components/ha-grid-size-picker.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "./ha-icon-button";
|
||||
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
|
||||
|
||||
import { mdiRestore } from "@mdi/js";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
type GridSizeValue = {
|
||||
rows?: number;
|
||||
columns?: number;
|
||||
};
|
||||
|
||||
@customElement("ha-grid-size-picker")
|
||||
export class HaGridSizeEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: GridSizeValue;
|
||||
|
||||
@property({ attribute: false }) public rows = 6;
|
||||
|
||||
@property({ attribute: false }) public columns = 4;
|
||||
|
||||
@property({ attribute: false }) public rowMin?: number;
|
||||
|
||||
@property({ attribute: false }) public rowMax?: number;
|
||||
|
||||
@property({ attribute: false }) public columnMin?: number;
|
||||
|
||||
@property({ attribute: false }) public columnMax?: number;
|
||||
|
||||
@property({ attribute: false }) public isDefault?: boolean;
|
||||
|
||||
@state() public _localValue?: GridSizeValue = undefined;
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
if (changedProperties.has("value")) {
|
||||
this._localValue = this.value;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="grid">
|
||||
<ha-grid-layout-slider
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.grid-size-picker.columns"
|
||||
)}
|
||||
id="columns"
|
||||
.min=${this.columnMin ?? 1}
|
||||
.max=${this.columnMax ?? this.columns}
|
||||
.range=${this.columns}
|
||||
.value=${this.value?.columns}
|
||||
@value-changed=${this._valueChanged}
|
||||
@slider-moved=${this._sliderMoved}
|
||||
></ha-grid-layout-slider>
|
||||
<ha-grid-layout-slider
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.grid-size-picker.rows"
|
||||
)}
|
||||
id="rows"
|
||||
.min=${this.rowMin ?? 1}
|
||||
.max=${this.rowMax ?? this.rows}
|
||||
.range=${this.rows}
|
||||
vertical
|
||||
.value=${this.value?.rows}
|
||||
@value-changed=${this._valueChanged}
|
||||
@slider-moved=${this._sliderMoved}
|
||||
></ha-grid-layout-slider>
|
||||
${!this.isDefault
|
||||
? html`
|
||||
<ha-icon-button
|
||||
@click=${this._reset}
|
||||
class="reset"
|
||||
.path=${mdiRestore}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.grid-size-picker.reset_default"
|
||||
)}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.grid-size-picker.reset_default"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class="preview"
|
||||
style=${styleMap({
|
||||
"--total-rows": this.rows,
|
||||
"--total-columns": this.columns,
|
||||
"--rows": this._localValue?.rows,
|
||||
"--columns": this._localValue?.columns,
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
${Array(this.rows * this.columns)
|
||||
.fill(0)
|
||||
.map((_, index) => {
|
||||
const row = Math.floor(index / this.columns) + 1;
|
||||
const column = (index % this.columns) + 1;
|
||||
const disabled =
|
||||
(this.rowMin !== undefined && row < this.rowMin) ||
|
||||
(this.rowMax !== undefined && row > this.rowMax) ||
|
||||
(this.columnMin !== undefined && column < this.columnMin) ||
|
||||
(this.columnMax !== undefined && column > this.columnMax);
|
||||
return html`
|
||||
<div
|
||||
class="cell"
|
||||
data-row=${row}
|
||||
data-column=${column}
|
||||
?disabled=${disabled}
|
||||
@click=${this._cellClick}
|
||||
></div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
<div class="selected">
|
||||
<div class="cell"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_cellClick(ev) {
|
||||
const cell = ev.currentTarget as HTMLElement;
|
||||
if (cell.getAttribute("disabled") !== null) return;
|
||||
const rows = Number(cell.getAttribute("data-row"));
|
||||
const columns = Number(cell.getAttribute("data-column"));
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { rows, columns },
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const key = ev.currentTarget.id;
|
||||
const newValue = {
|
||||
...this.value,
|
||||
[key]: ev.detail.value,
|
||||
};
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private _reset(ev) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
rows: undefined,
|
||||
columns: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _sliderMoved(ev) {
|
||||
ev.stopPropagation();
|
||||
const key = ev.currentTarget.id;
|
||||
const value = ev.detail.value;
|
||||
if (value === undefined) return;
|
||||
this._localValue = {
|
||||
...this.value,
|
||||
[key]: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"reset column-slider"
|
||||
"row-slider preview";
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
#columns {
|
||||
grid-area: column-slider;
|
||||
}
|
||||
#rows {
|
||||
grid-area: row-slider;
|
||||
}
|
||||
.reset {
|
||||
grid-area: reset;
|
||||
}
|
||||
.preview {
|
||||
position: relative;
|
||||
grid-area: preview;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
.preview > div {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--total-columns), 1fr);
|
||||
grid-template-rows: repeat(var(--total-rows), 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
.preview .cell {
|
||||
background-color: var(--disabled-color);
|
||||
grid-column: span 1;
|
||||
grid-row: span 1;
|
||||
border-radius: 4px;
|
||||
opacity: 0.2;
|
||||
cursor: pointer;
|
||||
}
|
||||
.preview .cell[disabled] {
|
||||
opacity: 0.05;
|
||||
cursor: initial;
|
||||
}
|
||||
.selected {
|
||||
pointer-events: none;
|
||||
}
|
||||
.selected .cell {
|
||||
background-color: var(--primary-color);
|
||||
grid-column: 1 / span var(--columns, 0);
|
||||
grid-row: 1 / span var(--rows, 0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-grid-size-picker": HaGridSizeEditor;
|
||||
}
|
||||
}
|
@@ -64,6 +64,12 @@ const SELECTOR_SCHEMAS = {
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const,
|
||||
floor: [
|
||||
{
|
||||
name: "multiple",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const,
|
||||
icon: [] as const,
|
||||
location: [] as const,
|
||||
media: [] as const,
|
||||
|
@@ -32,6 +32,7 @@ export class HaTemplateSelector extends LitElement {
|
||||
autocomplete-icons
|
||||
@value-changed=${this._handleChange}
|
||||
dir="ltr"
|
||||
linewrap
|
||||
></ha-code-editor>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
|
@@ -327,6 +327,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
for (const entityId of Object.keys(this.hass.states)) {
|
||||
if (
|
||||
entityId.startsWith("update.") &&
|
||||
!this.hass.entities[entityId]?.hidden &&
|
||||
updateCanInstall(this.hass.states[entityId] as UpdateEntity)
|
||||
) {
|
||||
updateCount++;
|
||||
|
@@ -138,6 +138,17 @@ export const adminChangePassword = (
|
||||
password,
|
||||
});
|
||||
|
||||
export const adminChangeUsername = (
|
||||
hass: HomeAssistant,
|
||||
userId: string,
|
||||
username: string
|
||||
) =>
|
||||
hass.callWS<void>({
|
||||
type: "config/auth_provider/homeassistant/admin_change_username",
|
||||
user_id: userId,
|
||||
username,
|
||||
});
|
||||
|
||||
export const deleteAllRefreshTokens = (
|
||||
hass: HomeAssistant,
|
||||
token_type?: RefreshTokenType,
|
||||
|
@@ -901,7 +901,7 @@ const tryDescribeCondition = (
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (condition.above && condition.below) {
|
||||
if (condition.above !== undefined && condition.below !== undefined) {
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.above-below`,
|
||||
{
|
||||
@@ -912,7 +912,7 @@ const tryDescribeCondition = (
|
||||
}
|
||||
);
|
||||
}
|
||||
if (condition.above) {
|
||||
if (condition.above !== undefined) {
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.above`,
|
||||
{
|
||||
@@ -922,7 +922,7 @@ const tryDescribeCondition = (
|
||||
}
|
||||
);
|
||||
}
|
||||
if (condition.below) {
|
||||
if (condition.below !== undefined) {
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.below`,
|
||||
{
|
||||
|
@@ -6,6 +6,7 @@ export interface ConfigUpdateValues {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
elevation: number;
|
||||
radius: number;
|
||||
unit_system: "metric" | "us_customary";
|
||||
time_zone: string;
|
||||
external_url?: string | null;
|
||||
|
@@ -249,6 +249,22 @@ export const localizeDeviceAutomationTrigger = (
|
||||
) ||
|
||||
(trigger.subtype ? `"${trigger.subtype}" ${trigger.type}` : trigger.type!);
|
||||
|
||||
export const localizeExtraFieldsComputeLabelCallback =
|
||||
(hass: HomeAssistant, deviceAutomation: DeviceAutomation) =>
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
(schema): string =>
|
||||
hass.localize(
|
||||
`component.${deviceAutomation.domain}.device_automation.extra_fields.${schema.name}`
|
||||
) || schema.name;
|
||||
|
||||
export const localizeExtraFieldsComputeHelperCallback =
|
||||
(hass: HomeAssistant, deviceAutomation: DeviceAutomation) =>
|
||||
// Returns a callback for ha-form to calculate helper texts per schema object
|
||||
(schema): string | undefined =>
|
||||
hass.localize(
|
||||
`component.${deviceAutomation.domain}.device_automation.extra_fields_descriptions.${schema.name}`
|
||||
);
|
||||
|
||||
export const sortDeviceAutomations = (
|
||||
automationA: DeviceAutomation,
|
||||
automationB: DeviceAutomation
|
||||
|
@@ -30,6 +30,7 @@ export interface LovelaceViewElement extends HTMLElement {
|
||||
export interface LovelaceSectionElement extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
lovelace?: Lovelace;
|
||||
preview?: boolean;
|
||||
viewIndex?: number;
|
||||
index?: number;
|
||||
cards?: HuiCard[];
|
||||
|
@@ -2,6 +2,7 @@ import { HomeAssistant } from "../types";
|
||||
|
||||
export interface OTBRInfo {
|
||||
active_dataset_tlvs: string;
|
||||
border_agent_id: string;
|
||||
channel: number;
|
||||
extended_address: string;
|
||||
url: string;
|
||||
|
21
src/data/threshold.ts
Normal file
21
src/data/threshold.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface ThresholdPreview {
|
||||
state: string;
|
||||
attributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export const subscribePreviewThreshold = (
|
||||
hass: HomeAssistant,
|
||||
flow_id: string,
|
||||
flow_type: "config_flow" | "options_flow",
|
||||
user_input: Record<string, any>,
|
||||
callback: (preview: ThresholdPreview) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(callback, {
|
||||
type: "threshold/start_preview",
|
||||
flow_id,
|
||||
flow_type,
|
||||
user_input,
|
||||
});
|
@@ -1,6 +1,9 @@
|
||||
import {
|
||||
mdiAlertCircleOutline,
|
||||
mdiGauge,
|
||||
mdiThermometer,
|
||||
mdiThermometerWater,
|
||||
mdiSunWireless,
|
||||
mdiWaterPercent,
|
||||
mdiWeatherCloudy,
|
||||
mdiWeatherFog,
|
||||
@@ -114,10 +117,15 @@ export const weatherIcons = {
|
||||
};
|
||||
|
||||
export const weatherAttrIcons = {
|
||||
apparent_temperature: mdiThermometer,
|
||||
cloud_coverage: mdiWeatherCloudy,
|
||||
dew_point: mdiThermometerWater,
|
||||
humidity: mdiWaterPercent,
|
||||
wind_bearing: mdiWeatherWindy,
|
||||
wind_speed: mdiWeatherWindy,
|
||||
pressure: mdiGauge,
|
||||
temperature: mdiThermometer,
|
||||
uv_index: mdiSunWireless,
|
||||
visibility: mdiWeatherFog,
|
||||
precipitation: mdiWeatherRainy,
|
||||
};
|
||||
@@ -221,6 +229,8 @@ export const getWeatherUnit = (
|
||||
stateObj.attributes.pressure_unit ||
|
||||
(lengthUnit === "km" ? "hPa" : "inHg")
|
||||
);
|
||||
case "apparent_temperature":
|
||||
case "dew_point":
|
||||
case "temperature":
|
||||
case "templow":
|
||||
return (
|
||||
@@ -228,6 +238,7 @@ export const getWeatherUnit = (
|
||||
);
|
||||
case "wind_speed":
|
||||
return stateObj.attributes.wind_speed_unit || `${lengthUnit}/h`;
|
||||
case "cloud_coverage":
|
||||
case "humidity":
|
||||
case "precipitation_probability":
|
||||
return "%";
|
||||
|
@@ -14,6 +14,7 @@ export interface Zone {
|
||||
export interface HomeZoneMutableParams {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
export interface ZoneMutableParams {
|
||||
|
108
src/dialogs/config-flow/previews/flow-preview-threshold.ts
Normal file
108
src/dialogs/config-flow/previews/flow-preview-threshold.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { FlowType } from "../../../data/data_entry_flow";
|
||||
import {
|
||||
ThresholdPreview,
|
||||
subscribePreviewThreshold,
|
||||
} from "../../../data/threshold";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "./entity-preview-row";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
@customElement("flow-preview-threshold")
|
||||
class FlowPreviewThreshold extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public flowType!: FlowType;
|
||||
|
||||
public handler!: string;
|
||||
|
||||
@property() public stepId!: string;
|
||||
|
||||
@property() public flowId!: string;
|
||||
|
||||
@property() public stepData!: Record<string, any>;
|
||||
|
||||
@state() private _preview?: HassEntity;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
willUpdate(changedProps) {
|
||||
if (changedProps.has("stepData")) {
|
||||
this._debouncedSubscribePreview();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._error) {
|
||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||
}
|
||||
return html`<entity-preview-row
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._preview}
|
||||
></entity-preview-row>`;
|
||||
}
|
||||
|
||||
private _setPreview = (preview: ThresholdPreview) => {
|
||||
const now = new Date().toISOString();
|
||||
this._preview = {
|
||||
entity_id: `${this.stepId}.___flow_preview___`,
|
||||
last_changed: now,
|
||||
last_updated: now,
|
||||
context: { id: "", parent_id: null, user_id: null },
|
||||
...preview,
|
||||
};
|
||||
};
|
||||
|
||||
private _debouncedSubscribePreview = debounce(() => {
|
||||
this._subscribePreview();
|
||||
}, 250);
|
||||
|
||||
private async _subscribePreview() {
|
||||
if (this._unsub) {
|
||||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this.flowType === "repair_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._unsub = subscribePreviewThreshold(
|
||||
this.hass,
|
||||
this.flowId,
|
||||
this.flowType,
|
||||
this.stepData,
|
||||
this._setPreview
|
||||
);
|
||||
await this._unsub;
|
||||
fireEvent(this, "set-flow-errors", { errors: {} });
|
||||
} catch (err: any) {
|
||||
if (typeof err.message === "string") {
|
||||
this._error = err.message;
|
||||
} else {
|
||||
this._error = undefined;
|
||||
fireEvent(this, "set-flow-errors", err.message);
|
||||
}
|
||||
this._unsub = undefined;
|
||||
this._preview = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"flow-preview-threshold": FlowPreviewThreshold;
|
||||
}
|
||||
}
|
@@ -126,7 +126,6 @@ class MoreInfoUpdate extends LitElement {
|
||||
></ha-checkbox>
|
||||
</ha-formfield> `
|
||||
: ""}
|
||||
<hr />
|
||||
<div class="actions">
|
||||
${this.stateObj.attributes.auto_update
|
||||
? ""
|
||||
@@ -240,10 +239,20 @@ class MoreInfoUpdate extends LitElement {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.actions {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
background: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
margin: 8px 0 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: 12px 0;
|
||||
margin-bottom: -24px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.actions mwc-button {
|
||||
|
@@ -2,7 +2,6 @@
|
||||
import "../resources/compatibility";
|
||||
import "../auth/ha-authorize";
|
||||
import "../resources/safari-14-attachshadow-patch";
|
||||
import "../resources/array.flat.polyfill";
|
||||
|
||||
import("../resources/ha-style");
|
||||
import("@polymer/polymer/lib/utils/settings").then(
|
||||
|
@@ -25,7 +25,6 @@ import { subscribePanels } from "../data/ws-panels";
|
||||
import { subscribeThemes } from "../data/ws-themes";
|
||||
import { subscribeUser } from "../data/ws-user";
|
||||
import type { ExternalAuth } from "../external_app/external_auth";
|
||||
import "../resources/array.flat.polyfill";
|
||||
import "../resources/safari-14-attachshadow-patch";
|
||||
|
||||
window.name = MAIN_WINDOW_NAME;
|
||||
|
@@ -2,7 +2,6 @@
|
||||
import "../resources/compatibility";
|
||||
import "../onboarding/ha-onboarding";
|
||||
import "../resources/safari-14-attachshadow-patch";
|
||||
import "../resources/array.flat.polyfill";
|
||||
|
||||
import("../resources/ha-style");
|
||||
import("@polymer/polymer/lib/utils/settings").then(
|
||||
|
@@ -5,6 +5,7 @@ export const demoConfig: HassConfig = {
|
||||
elevation: 300,
|
||||
latitude: 52.3731339,
|
||||
longitude: 4.8903147,
|
||||
radius: 100,
|
||||
unit_system: {
|
||||
length: "km",
|
||||
mass: "kg",
|
||||
|
@@ -96,7 +96,7 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
backPath="/config"
|
||||
back-path="/config"
|
||||
.tabs=${configSections.devices}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${this._getApplicationCredentials(
|
||||
|
@@ -12,6 +12,8 @@ import {
|
||||
deviceAutomationsEqual,
|
||||
DeviceCapabilities,
|
||||
fetchDeviceActionCapabilities,
|
||||
localizeExtraFieldsComputeLabelCallback,
|
||||
localizeExtraFieldsComputeHelperCallback,
|
||||
} from "../../../../../data/device_automation";
|
||||
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
@@ -84,8 +86,13 @@ export class HaDeviceAction extends LitElement {
|
||||
.data=${this._extraFieldsData(this.action, this._capabilities)}
|
||||
.schema=${this._capabilities.extra_fields}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._extraFieldsComputeLabelCallback(
|
||||
this.hass.localize
|
||||
.computeLabel=${localizeExtraFieldsComputeLabelCallback(
|
||||
this.hass,
|
||||
this.action
|
||||
)}
|
||||
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
|
||||
this.hass,
|
||||
this.action
|
||||
)}
|
||||
@value-changed=${this._extraFieldsChanged}
|
||||
></ha-form>
|
||||
@@ -152,14 +159,6 @@ export class HaDeviceAction extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _extraFieldsComputeLabelCallback(localize) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.actions.type.device_id.extra_fields.${schema.name}`
|
||||
) || schema.name;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-device-picker {
|
||||
display: block;
|
||||
|
@@ -12,6 +12,8 @@ import {
|
||||
DeviceCapabilities,
|
||||
DeviceCondition,
|
||||
fetchDeviceConditionCapabilities,
|
||||
localizeExtraFieldsComputeLabelCallback,
|
||||
localizeExtraFieldsComputeHelperCallback,
|
||||
} from "../../../../../data/device_automation";
|
||||
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
@@ -84,8 +86,13 @@ export class HaDeviceCondition extends LitElement {
|
||||
.data=${this._extraFieldsData(this.condition, this._capabilities)}
|
||||
.schema=${this._capabilities.extra_fields}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._extraFieldsComputeLabelCallback(
|
||||
this.hass.localize
|
||||
.computeLabel=${localizeExtraFieldsComputeLabelCallback(
|
||||
this.hass,
|
||||
this.condition
|
||||
)}
|
||||
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
|
||||
this.hass,
|
||||
this.condition
|
||||
)}
|
||||
@value-changed=${this._extraFieldsChanged}
|
||||
></ha-form>
|
||||
@@ -153,14 +160,6 @@ export class HaDeviceCondition extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _extraFieldsComputeLabelCallback(localize) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.device.extra_fields.${schema.name}`
|
||||
) || schema.name;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-device-picker {
|
||||
display: block;
|
||||
|
@@ -596,6 +596,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
private _showTriggeredInfo() {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.triggering_event_detail"
|
||||
),
|
||||
text: html`
|
||||
<ha-yaml-editor
|
||||
readOnly
|
||||
|
@@ -14,6 +14,8 @@ import {
|
||||
DeviceCapabilities,
|
||||
DeviceTrigger,
|
||||
fetchDeviceTriggerCapabilities,
|
||||
localizeExtraFieldsComputeLabelCallback,
|
||||
localizeExtraFieldsComputeHelperCallback,
|
||||
} from "../../../../../data/device_automation";
|
||||
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
@@ -88,8 +90,13 @@ export class HaDeviceTrigger extends LitElement {
|
||||
.data=${this._extraFieldsData(this.trigger, this._capabilities)}
|
||||
.schema=${this._capabilities.extra_fields}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._extraFieldsComputeLabelCallback(
|
||||
this.hass.localize
|
||||
.computeLabel=${localizeExtraFieldsComputeLabelCallback(
|
||||
this.hass,
|
||||
this.trigger
|
||||
)}
|
||||
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
|
||||
this.hass,
|
||||
this.trigger
|
||||
)}
|
||||
@value-changed=${this._extraFieldsChanged}
|
||||
></ha-form>
|
||||
@@ -177,14 +184,6 @@ export class HaDeviceTrigger extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _extraFieldsComputeLabelCallback(localize) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.device.extra_fields.${schema.name}`
|
||||
) || schema.name;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-device-picker {
|
||||
display: block;
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
mdiPower,
|
||||
mdiRefresh,
|
||||
} from "@mdi/js";
|
||||
import { HassEntities, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
@@ -177,7 +177,10 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { updates: canInstallUpdates, total: totalUpdates } =
|
||||
this._filterUpdateEntitiesWithInstall(this.hass.states);
|
||||
this._filterUpdateEntitiesWithInstall(
|
||||
this.hass.states,
|
||||
this.hass.entities
|
||||
);
|
||||
|
||||
const { issues: repairsIssues, total: totalRepairIssues } =
|
||||
this._repairsIssues;
|
||||
@@ -306,8 +309,13 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _filterUpdateEntitiesWithInstall = memoizeOne(
|
||||
(entities: HassEntities): { updates: UpdateEntity[]; total: number } => {
|
||||
const updates = filterUpdateEntitiesWithInstall(entities);
|
||||
(
|
||||
entities: HomeAssistant["states"],
|
||||
entityRegistry: HomeAssistant["entities"]
|
||||
): { updates: UpdateEntity[]; total: number } => {
|
||||
const updates = filterUpdateEntitiesWithInstall(entities).filter(
|
||||
(entity) => !entityRegistry[entity.entity_id]?.hidden
|
||||
);
|
||||
|
||||
return {
|
||||
updates: updates.slice(0, updates.length === 3 ? updates.length : 2),
|
||||
|
@@ -24,7 +24,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { SENSOR_ENTITIES } from "../../../common/const";
|
||||
import { SENSOR_ENTITIES, ASSIST_ENTITIES } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
@@ -190,26 +190,42 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
private _entitiesByCategory = memoizeOne(
|
||||
(entities: EntityRegistryEntry[]) => {
|
||||
const result = groupBy(entities, (entry) =>
|
||||
entry.entity_category
|
||||
? entry.entity_category
|
||||
: computeDomain(entry.entity_id) === "event"
|
||||
? "event"
|
||||
: SENSOR_ENTITIES.includes(computeDomain(entry.entity_id))
|
||||
? "sensor"
|
||||
: "control"
|
||||
) as Record<
|
||||
const result = groupBy(entities, (entry) => {
|
||||
const domain = computeDomain(entry.entity_id);
|
||||
|
||||
if (entry.entity_category) {
|
||||
return entry.entity_category;
|
||||
}
|
||||
|
||||
if (domain === "event" || domain === "notify") {
|
||||
return domain;
|
||||
}
|
||||
|
||||
if (SENSOR_ENTITIES.includes(domain)) {
|
||||
return "sensor";
|
||||
}
|
||||
|
||||
if (ASSIST_ENTITIES.includes(domain)) {
|
||||
return "assist";
|
||||
}
|
||||
|
||||
return "control";
|
||||
}) as Record<
|
||||
| "control"
|
||||
| "event"
|
||||
| "sensor"
|
||||
| "assist"
|
||||
| "notify"
|
||||
| NonNullable<EntityRegistryEntry["entity_category"]>,
|
||||
EntityRegistryStateEntry[]
|
||||
>;
|
||||
for (const key of [
|
||||
"assist",
|
||||
"config",
|
||||
"control",
|
||||
"diagnostic",
|
||||
"event",
|
||||
"notify",
|
||||
"sensor",
|
||||
]) {
|
||||
if (!(key in result)) {
|
||||
@@ -854,7 +870,15 @@ export class HaConfigDevicePage extends LitElement {
|
||||
</div>
|
||||
<div class="column">
|
||||
${(
|
||||
["control", "sensor", "event", "config", "diagnostic"] as const
|
||||
[
|
||||
"control",
|
||||
"sensor",
|
||||
"notify",
|
||||
"event",
|
||||
"assist",
|
||||
"config",
|
||||
"diagnostic",
|
||||
] as const
|
||||
).map((category) =>
|
||||
// Make sure we render controls if no other cards will be rendered
|
||||
entitiesByCategory[category].length > 0 ||
|
||||
@@ -1004,6 +1028,9 @@ export class HaConfigDevicePage extends LitElement {
|
||||
: this.hass.localize(
|
||||
`ui.panel.config.devices.confirm_delete`
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
|
@@ -164,6 +164,9 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
text: this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.confirm_delete"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
|
@@ -215,6 +215,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
text: this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.confirm_delete"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
|
@@ -839,7 +839,7 @@ ${
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.button"
|
||||
"ui.panel.config.entities.picker.delete_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>
|
||||
@@ -1256,25 +1256,23 @@ ${rejected
|
||||
});
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.config.entities.picker.remove_selected.confirm_${
|
||||
removeableEntities.length !== this._selected.length ? "partly_" : ""
|
||||
}title`,
|
||||
{ number: removeableEntities.length }
|
||||
`ui.panel.config.entities.picker.delete_selected.confirm_title`
|
||||
),
|
||||
text:
|
||||
removeableEntities.length === this._selected.length
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.confirm_text"
|
||||
"ui.panel.config.entities.picker.delete_selected.confirm_text"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.confirm_partly_text",
|
||||
"ui.panel.config.entities.picker.delete_selected.confirm_partly_text",
|
||||
{
|
||||
removable: removeableEntities.length,
|
||||
deletable: removeableEntities.length,
|
||||
selected: this._selected.length,
|
||||
}
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.remove"),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
confirm: () => {
|
||||
removeableEntities.forEach((entity) =>
|
||||
removeEntityRegistryEntry(this.hass, entity)
|
||||
|
@@ -754,7 +754,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
${item.disabled_by && devices.length
|
||||
? html`
|
||||
<ha-menu-item
|
||||
.href=${devices.length === 1
|
||||
href=${devices.length === 1
|
||||
? `/config/devices/device/${devices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
@@ -769,7 +769,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
: ""}
|
||||
${item.disabled_by && services.length
|
||||
? html`<ha-menu-item
|
||||
.href=${services.length === 1
|
||||
href=${services.length === 1
|
||||
? `/config/devices/device/${services[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
@@ -787,7 +787,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
${item.disabled_by && entities.length
|
||||
? html`
|
||||
<ha-menu-item
|
||||
.href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiShapeOutline}
|
||||
@@ -827,7 +827,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
${this._diagnosticHandler && item.state === "loaded"
|
||||
? html`
|
||||
<ha-menu-item
|
||||
.href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
|
||||
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
|
||||
target="_blank"
|
||||
@click=${this._signUrl}
|
||||
>
|
||||
@@ -1414,14 +1414,17 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
ha-alert:first-of-type {
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-list-item-new {
|
||||
position: relative;
|
||||
}
|
||||
ha-list-item-new.discovered {
|
||||
height: 72px;
|
||||
}
|
||||
ha-list-item-new.config_entry::after {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 8px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0.12;
|
||||
pointer-events: none;
|
||||
|
@@ -36,14 +36,8 @@ class DialogThreadDataset extends LitElement implements HassDialog {
|
||||
dataset.extended_pan_id &&
|
||||
otbrInfo.active_dataset_tlvs?.includes(dataset.extended_pan_id);
|
||||
|
||||
const canImportKeychain =
|
||||
hasOTBR &&
|
||||
!this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain &&
|
||||
network.routers?.length;
|
||||
|
||||
return html`<ha-dialog
|
||||
open
|
||||
.hideActions=${!canImportKeychain}
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(this.hass, network.name)}
|
||||
>
|
||||
@@ -59,28 +53,8 @@ class DialogThreadDataset extends LitElement implements HassDialog {
|
||||
Active dataset TLVs: ${otbrInfo.active_dataset_tlvs}`
|
||||
: nothing}
|
||||
</div>
|
||||
${canImportKeychain
|
||||
? html`<ha-button slot="primary-action" @click=${this._sendCredentials}
|
||||
>Send credentials to phone</ha-button
|
||||
>`
|
||||
: nothing}
|
||||
</ha-dialog>`;
|
||||
}
|
||||
|
||||
private _sendCredentials() {
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "thread/store_in_platform_keychain",
|
||||
payload: {
|
||||
mac_extended_address:
|
||||
this._params?.network.dataset?.preferred_extended_address ||
|
||||
this._params!.network.routers![0]!.extended_address,
|
||||
border_agent_id:
|
||||
this._params?.network.dataset?.preferred_border_agent_id ||
|
||||
this._params!.network.routers![0]!.border_agent_id,
|
||||
active_operational_dataset: this._params!.otbrInfo!.active_dataset_tlvs,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -151,7 +151,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
slot="fab"
|
||||
@click=${this._importExternalThreadCredentials}
|
||||
extended
|
||||
label="Import credentials"
|
||||
label="Send credentials to Home Assistant"
|
||||
><ha-svg-icon slot="icon" .path=${mdiCellphoneKey}></ha-svg-icon
|
||||
></ha-fab>`
|
||||
: nothing}
|
||||
@@ -160,6 +160,14 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _renderNetwork(network: ThreadNetwork) {
|
||||
const canImportKeychain =
|
||||
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain &&
|
||||
network.dataset?.extended_pan_id &&
|
||||
this._otbrInfo &&
|
||||
this._otbrInfo?.active_dataset_tlvs?.includes(
|
||||
network.dataset.extended_pan_id
|
||||
);
|
||||
|
||||
return html`<ha-card>
|
||||
<div class="card-header">
|
||||
${network.name}${network.dataset
|
||||
@@ -303,9 +311,30 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
</div>`
|
||||
: ""}
|
||||
${canImportKeychain
|
||||
? html`<div class="card-actions">
|
||||
<mwc-button @click=${this._sendCredentials}
|
||||
>Send credentials to phone</mwc-button
|
||||
>
|
||||
</div>`
|
||||
: ""}
|
||||
</ha-card>`;
|
||||
}
|
||||
|
||||
private _sendCredentials() {
|
||||
if (!this._otbrInfo) {
|
||||
return;
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "thread/store_in_platform_keychain",
|
||||
payload: {
|
||||
mac_extended_address: this._otbrInfo.extended_address,
|
||||
border_agent_id: this._otbrInfo.border_agent_id ?? "",
|
||||
active_operational_dataset: this._otbrInfo.active_dataset_tlvs ?? "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _showDatasetInfo(ev: Event) {
|
||||
const network = (ev.currentTarget as any).network as ThreadNetwork;
|
||||
showThreadDatasetDialog(this, { network, otbrInfo: this._otbrInfo });
|
||||
|
@@ -3,6 +3,7 @@ import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-alert";
|
||||
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
|
||||
@@ -60,7 +61,8 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
| "finished"
|
||||
| "provisioned"
|
||||
| "validate_dsk_enter_pin"
|
||||
| "grant_security_classes";
|
||||
| "grant_security_classes"
|
||||
| "waiting_for_device";
|
||||
|
||||
@state() private _device?: ZWaveJSAddNodeDevice;
|
||||
|
||||
@@ -86,6 +88,11 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
|
||||
private _qrProcessing = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("beforeunload", this._onBeforeUnload);
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribe();
|
||||
@@ -106,14 +113,22 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
// Prevent accidentally closing the dialog in certain stages
|
||||
const preventClose = this._shouldPreventClose();
|
||||
|
||||
const heading = this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.title"
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.zwave_js.add_node.title")
|
||||
)}
|
||||
.heading=${preventClose
|
||||
? heading
|
||||
: createCloseHeading(this.hass, heading)}
|
||||
scrimClickAction=${ifDefined(preventClose ? "" : undefined)}
|
||||
escapeKeyAction=${ifDefined(preventClose ? "" : undefined)}
|
||||
>
|
||||
${this._status === "loading"
|
||||
? html`<div style="display: flex; justify-content: center;">
|
||||
@@ -122,81 +137,93 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
</div>`
|
||||
: this._status === "choose_strategy"
|
||||
? html`<h3>Choose strategy</h3>
|
||||
<div class="flex-column">
|
||||
<ha-formfield
|
||||
.label=${html`<b>Secure if possible</b>
|
||||
<div class="secondary">
|
||||
Requires user interaction during inclusion. Fast and
|
||||
secure with S2 when supported. Fallback to legacy S0 or
|
||||
no encryption when necessary.
|
||||
</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="strategy"
|
||||
@change=${this._handleStrategyChange}
|
||||
.value=${InclusionStrategy.Default}
|
||||
.checked=${this._inclusionStrategy ===
|
||||
InclusionStrategy.Default ||
|
||||
this._inclusionStrategy === undefined}
|
||||
: this._status === "waiting_for_device"
|
||||
? html`<div class="flex-container">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.waiting_for_device"
|
||||
)}
|
||||
</p>
|
||||
</div>`
|
||||
: this._status === "choose_strategy"
|
||||
? html`<h3>Choose strategy</h3>
|
||||
<div class="flex-column">
|
||||
<ha-formfield
|
||||
.label=${html`<b>Secure if possible</b>
|
||||
<div class="secondary">
|
||||
Requires user interaction during inclusion. Fast and
|
||||
secure with S2 when supported. Fallback to legacy S0
|
||||
or no encryption when necessary.
|
||||
</div>`}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`<b>Legacy Secure</b>
|
||||
<div class="secondary">
|
||||
Uses the older S0 security that is secure, but slow due
|
||||
to a lot of overhead. Allows securely including S2
|
||||
capable devices which fail to be included with S2.
|
||||
</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="strategy"
|
||||
@change=${this._handleStrategyChange}
|
||||
.value=${InclusionStrategy.Security_S0}
|
||||
.checked=${this._inclusionStrategy ===
|
||||
InclusionStrategy.Security_S0}
|
||||
<ha-radio
|
||||
name="strategy"
|
||||
@change=${this._handleStrategyChange}
|
||||
.value=${InclusionStrategy.Default}
|
||||
.checked=${this._inclusionStrategy ===
|
||||
InclusionStrategy.Default ||
|
||||
this._inclusionStrategy === undefined}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`<b>Legacy Secure</b>
|
||||
<div class="secondary">
|
||||
Uses the older S0 security that is secure, but slow
|
||||
due to a lot of overhead. Allows securely including S2
|
||||
capable devices which fail to be included with S2.
|
||||
</div>`}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`<b>Insecure</b>
|
||||
<div class="secondary">Do not use encryption.</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="strategy"
|
||||
@change=${this._handleStrategyChange}
|
||||
.value=${InclusionStrategy.Insecure}
|
||||
.checked=${this._inclusionStrategy ===
|
||||
InclusionStrategy.Insecure}
|
||||
<ha-radio
|
||||
name="strategy"
|
||||
@change=${this._handleStrategyChange}
|
||||
.value=${InclusionStrategy.Security_S0}
|
||||
.checked=${this._inclusionStrategy ===
|
||||
InclusionStrategy.Security_S0}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`<b>Insecure</b>
|
||||
<div class="secondary">Do not use encryption.</div>`}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._startManualInclusion}
|
||||
>
|
||||
Search device
|
||||
</mwc-button>`
|
||||
: this._status === "qr_scan"
|
||||
? html`${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
: ""}
|
||||
<ha-qr-scanner
|
||||
.localize=${this.hass.localize}
|
||||
@qr-code-scanned=${this._qrCodeScanned}
|
||||
></ha-qr-scanner>
|
||||
<mwc-button slot="secondaryAction" @click=${this._startOver}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.common.back"
|
||||
)}
|
||||
<ha-radio
|
||||
name="strategy"
|
||||
@change=${this._handleStrategyChange}
|
||||
.value=${InclusionStrategy.Insecure}
|
||||
.checked=${this._inclusionStrategy ===
|
||||
InclusionStrategy.Insecure}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._startManualInclusion}
|
||||
>
|
||||
Search device
|
||||
</mwc-button>`
|
||||
: this._status === "validate_dsk_enter_pin"
|
||||
? html`
|
||||
: this._status === "qr_scan"
|
||||
? html`${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
: ""}
|
||||
<ha-qr-scanner
|
||||
.localize=${this.hass.localize}
|
||||
@qr-code-scanned=${this._qrCodeScanned}
|
||||
></ha-qr-scanner>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._startOver}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.common.back"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: this._status === "validate_dsk_enter_pin"
|
||||
? html`
|
||||
<p>
|
||||
Please enter the 5-digit PIN for your device and verify that
|
||||
the rest of the device-specific key matches the one that can
|
||||
@@ -225,198 +252,160 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: this._status === "grant_security_classes"
|
||||
? html`
|
||||
<h3>
|
||||
The device has requested the following security classes:
|
||||
</h3>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
: ""}
|
||||
<div class="flex-column">
|
||||
${this._requestedGrant?.securityClasses
|
||||
.sort((a, b) => {
|
||||
// Put highest security classes at the top, S0 at the bottom
|
||||
if (a === SecurityClass.S0_Legacy) return 1;
|
||||
if (b === SecurityClass.S0_Legacy) return -1;
|
||||
return b - a;
|
||||
})
|
||||
.map(
|
||||
(securityClass) =>
|
||||
html`<ha-formfield
|
||||
.label=${html`<b
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title`
|
||||
)}</b
|
||||
>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description`
|
||||
)}
|
||||
</div>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
@change=${this._handleSecurityClassChange}
|
||||
.value=${securityClass}
|
||||
.checked=${this._securityClasses.includes(
|
||||
securityClass
|
||||
)}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
)}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!this._securityClasses.length}
|
||||
@click=${this._grantSecurityClasses}
|
||||
>
|
||||
Submit
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "timed_out"
|
||||
: this._status === "grant_security_classes"
|
||||
? html`
|
||||
<h3>Timed out!</h3>
|
||||
<p>
|
||||
We have not found any device in inclusion mode. Make
|
||||
sure the device is active and in inclusion mode.
|
||||
</p>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._startOver}
|
||||
>
|
||||
Retry
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "started_specific"
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||
)}
|
||||
</h3>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||
)}
|
||||
</p>`
|
||||
: this._status === "started"
|
||||
? html`
|
||||
<div class="select-inclusion">
|
||||
<div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||
)}
|
||||
</h2>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<button
|
||||
class="link"
|
||||
@click=${this._chooseInclusionStrategy}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
|
||||
)}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
${this._supportsSmartStart
|
||||
? html` <div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code"
|
||||
)}
|
||||
</h2>
|
||||
<ha-svg-icon
|
||||
.path=${mdiQrcodeScan}
|
||||
></ha-svg-icon>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code_paragraph"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<mwc-button @click=${this._scanQRCode}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.scan_qr_code"
|
||||
)}
|
||||
</mwc-button>
|
||||
</p>
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "interviewing"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
<div class="status">
|
||||
<p>
|
||||
<b
|
||||
<h3>
|
||||
The device has requested the following security
|
||||
classes:
|
||||
</h3>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
: ""}
|
||||
<div class="flex-column">
|
||||
${this._requestedGrant?.securityClasses
|
||||
.sort((a, b) => {
|
||||
// Put highest security classes at the top, S0 at the bottom
|
||||
if (a === SecurityClass.S0_Legacy) return 1;
|
||||
if (b === SecurityClass.S0_Legacy) return -1;
|
||||
return b - a;
|
||||
})
|
||||
.map(
|
||||
(securityClass) =>
|
||||
html`<ha-formfield
|
||||
.label=${html`<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.interview_started"
|
||||
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title`
|
||||
)}</b
|
||||
>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description`
|
||||
)}
|
||||
</div>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
@change=${this._handleSecurityClassChange}
|
||||
.value=${securityClass}
|
||||
.checked=${this._securityClasses.includes(
|
||||
securityClass
|
||||
)}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
)}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!this._securityClasses.length}
|
||||
@click=${this._grantSecurityClasses}
|
||||
>
|
||||
Submit
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "timed_out"
|
||||
? html`
|
||||
<h3>Timed out!</h3>
|
||||
<p>
|
||||
We have not found any device in inclusion mode. Make
|
||||
sure the device is active and in inclusion mode.
|
||||
</p>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._startOver}
|
||||
>
|
||||
Retry
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "started_specific"
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||
)}
|
||||
</h3>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||
)}
|
||||
</p>`
|
||||
: this._status === "started"
|
||||
? html`
|
||||
<div class="select-inclusion">
|
||||
<div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||
)}
|
||||
</h2>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<button
|
||||
class="link"
|
||||
@click=${this._chooseInclusionStrategy}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
|
||||
)}
|
||||
</button>
|
||||
</p>
|
||||
${this._stages
|
||||
? html` <div class="stages">
|
||||
${this._stages.map(
|
||||
(stage) => html`
|
||||
<span class="stage">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
${stage}
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
${this._supportsSmartStart
|
||||
? html` <div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code"
|
||||
)}
|
||||
</h2>
|
||||
<ha-svg-icon
|
||||
.path=${mdiQrcodeScan}
|
||||
></ha-svg-icon>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code_paragraph"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<mwc-button @click=${this._scanQRCode}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.scan_qr_code"
|
||||
)}
|
||||
</mwc-button>
|
||||
</p>
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "failed"
|
||||
: this._status === "interviewing"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
<div class="status">
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.inclusion_failed"
|
||||
)}
|
||||
>
|
||||
${this._error ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.check_logs"
|
||||
)}
|
||||
</ha-alert>
|
||||
<p>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.interview_started"
|
||||
)}</b
|
||||
>
|
||||
</p>
|
||||
${this._stages
|
||||
? html` <div class="stages">
|
||||
${this._stages.map(
|
||||
@@ -441,45 +430,21 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "finished"
|
||||
: this._status === "failed"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${this._lowSecurity
|
||||
? mdiAlertCircle
|
||||
: mdiCheckCircle}
|
||||
class=${this._lowSecurity
|
||||
? "warning"
|
||||
: "success"}
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.inclusion_finished"
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.inclusion_failed"
|
||||
)}
|
||||
</p>
|
||||
${this._lowSecurity
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
title="The device was added insecurely"
|
||||
>
|
||||
There was an error during secure
|
||||
inclusion. You can try again by
|
||||
excluding the device and adding it
|
||||
again.
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
<a
|
||||
href=${`/config/devices/device/${
|
||||
this._device!.id
|
||||
}`}
|
||||
>
|
||||
<mwc-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.view_device"
|
||||
)}
|
||||
</mwc-button>
|
||||
</a>
|
||||
${this._error ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.check_logs"
|
||||
)}
|
||||
</ha-alert>
|
||||
${this._stages
|
||||
? html` <div class="stages">
|
||||
${this._stages.map(
|
||||
@@ -504,18 +469,60 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "provisioned"
|
||||
? html` <div class="flex-container">
|
||||
: this._status === "finished"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
.path=${this._lowSecurity
|
||||
? mdiAlertCircle
|
||||
: mdiCheckCircle}
|
||||
class=${this._lowSecurity
|
||||
? "warning"
|
||||
: "success"}
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.provisioning_finished"
|
||||
"ui.panel.config.zwave_js.add_node.inclusion_finished"
|
||||
)}
|
||||
</p>
|
||||
${this._lowSecurity
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
title="The device was added insecurely"
|
||||
>
|
||||
There was an error during secure
|
||||
inclusion. You can try again by
|
||||
excluding the device and adding it
|
||||
again.
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
<a
|
||||
href=${`/config/devices/device/${
|
||||
this._device?.id
|
||||
}`}
|
||||
>
|
||||
<mwc-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.view_device"
|
||||
)}
|
||||
</mwc-button>
|
||||
</a>
|
||||
${this._stages
|
||||
? html` <div class="stages">
|
||||
${this._stages.map(
|
||||
(stage) => html`
|
||||
<span class="stage">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
${stage}
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button
|
||||
@@ -523,12 +530,42 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "provisioned"
|
||||
? html` <div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.provisioning_finished"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _shouldPreventClose(): boolean {
|
||||
return (
|
||||
this._status === "started_specific" ||
|
||||
this._status === "validate_dsk_enter_pin" ||
|
||||
this._status === "grant_security_classes" ||
|
||||
this._status === "waiting_for_device"
|
||||
);
|
||||
}
|
||||
|
||||
private _chooseInclusionStrategy(): void {
|
||||
this._unsubscribe();
|
||||
this._status = "choose_strategy";
|
||||
@@ -639,7 +676,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
}
|
||||
|
||||
private async _validateDskAndEnterPin(): Promise<void> {
|
||||
this._status = "loading";
|
||||
this._status = "waiting_for_device";
|
||||
this._error = undefined;
|
||||
try {
|
||||
await zwaveValidateDskAndEnterPin(
|
||||
@@ -656,7 +693,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
}
|
||||
|
||||
private async _grantSecurityClasses(): Promise<void> {
|
||||
this._status = "loading";
|
||||
this._status = "waiting_for_device";
|
||||
this._error = undefined;
|
||||
try {
|
||||
await zwaveGrantSecurityClasses(
|
||||
@@ -719,6 +756,12 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._addNodeTimeoutHandle = undefined;
|
||||
}
|
||||
|
||||
if (message.event === "node found") {
|
||||
// The user may have to enter a PIN. Until then prevent accidentally
|
||||
// closing the dialog
|
||||
this._status = "waiting_for_device";
|
||||
}
|
||||
|
||||
if (message.event === "validate dsk and enter pin") {
|
||||
this._status = "validate_dsk_enter_pin";
|
||||
this._dsk = message.dsk;
|
||||
@@ -775,6 +818,13 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
}, 90000);
|
||||
}
|
||||
|
||||
private _onBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
if (this._shouldPreventClose()) {
|
||||
event.preventDefault();
|
||||
}
|
||||
event.returnValue = true;
|
||||
};
|
||||
|
||||
private _unsubscribe(): void {
|
||||
if (this._subscribed) {
|
||||
this._subscribed.then((unsub) => unsub());
|
||||
@@ -791,6 +841,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
clearTimeout(this._addNodeTimeoutHandle);
|
||||
}
|
||||
this._addNodeTimeoutHandle = undefined;
|
||||
window.removeEventListener("beforeunload", this._onBeforeUnload);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
|
@@ -2,17 +2,21 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import {
|
||||
mdiCheck,
|
||||
mdiCheckCircleOutline,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiOpenInNew,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiStar,
|
||||
} from "@mdi/js";
|
||||
import { LitElement, PropertyValues, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoize from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
@@ -22,6 +26,9 @@ import "../../../../components/ha-clickable-list-item";
|
||||
import "../../../../components/ha-fab";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-menu";
|
||||
import type { HaMenu } from "../../../../components/ha-menu";
|
||||
import "../../../../components/ha-menu-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { LovelacePanelConfig } from "../../../../data/lovelace";
|
||||
import {
|
||||
@@ -41,13 +48,11 @@ import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { HomeAssistant, Route } from "../../../../types";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { getLovelaceStrategy } from "../../../lovelace/strategies/get-strategy";
|
||||
import { showNewDashboardDialog } from "../../dashboard/show-dialog-new-dashboard";
|
||||
import { lovelaceTabs } from "../ha-config-lovelace";
|
||||
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
type DataTableItem = Pick<
|
||||
LovelaceDashboard,
|
||||
@@ -85,6 +90,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@state() private _overflowDashboard?: LovelaceDashboard;
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu!: HaMenu;
|
||||
|
||||
public willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
@@ -210,40 +219,36 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
columns.url_path = {
|
||||
columns.actions = {
|
||||
title: "",
|
||||
label: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.url"
|
||||
),
|
||||
filterable: true,
|
||||
width: "100px",
|
||||
template: (dashboard) =>
|
||||
narrow
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiOpenInNew}
|
||||
.urlPath=${dashboard.url_path}
|
||||
@click=${this._navigate}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.open"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button
|
||||
.urlPath=${dashboard.url_path}
|
||||
@click=${this._navigate}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.open"
|
||||
)}</mwc-button
|
||||
>
|
||||
`,
|
||||
width: "64px",
|
||||
type: "icon-button",
|
||||
template: (dashboard) => html`
|
||||
<ha-icon-button
|
||||
.dashboard=${dashboard}
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
@click=${this._showOverflowMenu}
|
||||
></ha-icon-button>
|
||||
`,
|
||||
};
|
||||
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
private _showOverflowMenu = (ev) => {
|
||||
if (
|
||||
this._overflowMenu.open &&
|
||||
ev.target === this._overflowMenu.anchorElement
|
||||
) {
|
||||
this._overflowMenu.close();
|
||||
return;
|
||||
}
|
||||
this._overflowDashboard = ev.target.dashboard;
|
||||
this._overflowMenu.anchorElement = ev.target;
|
||||
this._overflowMenu.show();
|
||||
};
|
||||
|
||||
private _getItems = memoize((dashboards: LovelaceDashboard[]) => {
|
||||
const defaultMode = (
|
||||
this.hass.panels?.lovelace?.config as LovelacePanelConfig
|
||||
@@ -314,7 +319,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@row-click=${this._editDashboard}
|
||||
@row-click=${this._navigate}
|
||||
id="url_path"
|
||||
hasFab
|
||||
clickable
|
||||
@@ -346,6 +351,22 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-menu id="overflow-menu" positioning="fixed">
|
||||
<ha-menu-item @click=${this._editDashboard}>
|
||||
<ha-svg-icon .path=${mdiPencil} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">Edit</div>
|
||||
</ha-menu-item>
|
||||
|
||||
<ha-menu-item>
|
||||
<ha-svg-icon .path=${mdiStar} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">Set to default</div>
|
||||
</ha-menu-item>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-menu-item class="warning">
|
||||
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">Delete</div>
|
||||
</ha-menu-item>
|
||||
</ha-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -358,21 +379,23 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
this._dashboards = await fetchDashboards(this.hass);
|
||||
}
|
||||
|
||||
private _navigate(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
navigate(`/${(ev.target as any).urlPath}`);
|
||||
private _navigate(ev: CustomEvent) {
|
||||
const urlPath = (ev.detail as RowClickedEvent).id;
|
||||
navigate(`/${urlPath}`);
|
||||
}
|
||||
|
||||
private _editDashboard(ev: CustomEvent) {
|
||||
const urlPath = (ev.detail as RowClickedEvent).id;
|
||||
private _editDashboard = (ev) => {
|
||||
ev.stopPropagation();
|
||||
const dashboard = ev.currentTarget.parentElement.anchorElement.automation;
|
||||
|
||||
const urlPath = (ev.currentTarget as any).urlPath;
|
||||
|
||||
if (urlPath === "energy") {
|
||||
navigate("/config/energy");
|
||||
return;
|
||||
}
|
||||
const dashboard = this._dashboards.find((res) => res.url_path === urlPath);
|
||||
this._openDetailDialog(dashboard, urlPath);
|
||||
}
|
||||
};
|
||||
|
||||
private async _addDashboard() {
|
||||
showNewDashboardDialog(this, {
|
||||
|
@@ -8,6 +8,7 @@ import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-picture-upload";
|
||||
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
|
||||
import "../../../components/ha-textfield";
|
||||
import { adminChangeUsername } from "../../../data/auth";
|
||||
import { PersonMutableParams } from "../../../data/person";
|
||||
import {
|
||||
deleteUser,
|
||||
@@ -19,10 +20,11 @@ import {
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
import { ValueChangedEvent, HomeAssistant } from "../../../types";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showAddUserDialog } from "../users/show-dialog-add-user";
|
||||
import { showAdminChangePasswordDialog } from "../users/show-dialog-admin-change-password";
|
||||
@@ -136,9 +138,9 @@ class DialogPersonDetail extends LitElement {
|
||||
></ha-picture-upload>
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass!.localize(
|
||||
.label=${`${this.hass!.localize(
|
||||
"ui.panel.config.person.detail.allow_login"
|
||||
)}
|
||||
)}${this._user ? ` (${this._user.username})` : ""}`}
|
||||
>
|
||||
<ha-switch
|
||||
@change=${this._allowLoginChanged}
|
||||
@@ -244,13 +246,21 @@ class DialogPersonDetail extends LitElement {
|
||||
</mwc-button>
|
||||
${this._user && this.hass.user?.is_owner
|
||||
? html`<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._changePassword}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
</mwc-button>`
|
||||
slot="secondaryAction"
|
||||
@click=${this._changeUsername}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_username"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._changePassword}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
`
|
||||
: nothing}
|
||||
@@ -292,11 +302,14 @@ class DialogPersonDetail extends LitElement {
|
||||
userAddedCallback: async (user?: User) => {
|
||||
if (user) {
|
||||
target.checked = true;
|
||||
if (this._params!.entry) {
|
||||
await this._params!.updateEntry({ user_id: user.id });
|
||||
}
|
||||
this._params?.refreshUsers();
|
||||
this._user = user;
|
||||
this._userId = user.id;
|
||||
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
|
||||
this._localOnly = user.local_only;
|
||||
this._params?.refreshUsers();
|
||||
}
|
||||
},
|
||||
name: this._name,
|
||||
@@ -320,6 +333,9 @@ class DialogPersonDetail extends LitElement {
|
||||
await deleteUser(this.hass, this._userId);
|
||||
this._params?.refreshUsers();
|
||||
this._userId = undefined;
|
||||
this._user = undefined;
|
||||
this._isAdmin = undefined;
|
||||
this._localOnly = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,6 +365,53 @@ class DialogPersonDetail extends LitElement {
|
||||
showAdminChangePasswordDialog(this, { userId: this._user.id });
|
||||
}
|
||||
|
||||
private async _changeUsername() {
|
||||
if (!this._user) {
|
||||
return;
|
||||
}
|
||||
const credential = this._user.credentials.find(
|
||||
(cred) => cred.type === "homeassistant"
|
||||
);
|
||||
if (!credential) {
|
||||
showAlertDialog(this, {
|
||||
title: "No Home Assistant credentials found.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newUsername = await showPromptDialog(this, {
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.new_username"
|
||||
),
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.change"
|
||||
),
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.caption"
|
||||
),
|
||||
defaultValue: this._user.username!,
|
||||
});
|
||||
if (newUsername) {
|
||||
try {
|
||||
await adminChangeUsername(this.hass, this._user.id, newUsername);
|
||||
this._params?.refreshUsers();
|
||||
this._user = { ...this._user, username: newUsername };
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.username_changed"
|
||||
),
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.failed"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
|
@@ -31,8 +31,6 @@ class DialogIntegrationStartup extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
|
@@ -143,8 +143,6 @@ class DialogSystemInformation extends LitElement {
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.repairs.system_information")
|
||||
|
@@ -10,12 +10,16 @@ import "../../../components/ha-label";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import { adminChangeUsername } from "../../../data/auth";
|
||||
import {
|
||||
computeUserBadges,
|
||||
SYSTEM_GROUP_ID_ADMIN,
|
||||
SYSTEM_GROUP_ID_USER,
|
||||
} from "../../../data/user";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showAdminChangePasswordDialog } from "./show-dialog-admin-change-password";
|
||||
@@ -172,11 +176,15 @@ class DialogUserDetail extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
${!user.system_generated && this.hass.user?.is_owner
|
||||
? html`<mwc-button @click=${this._changePassword}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
</mwc-button>`
|
||||
? html`<mwc-button @click=${this._changeUsername}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_username"
|
||||
)} </mwc-button
|
||||
><mwc-button @click=${this._changePassword}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
@@ -250,6 +258,56 @@ class DialogUserDetail extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _changeUsername() {
|
||||
const credential = this._params?.entry.credentials.find(
|
||||
(cred) => cred.type === "homeassistant"
|
||||
);
|
||||
if (!credential) {
|
||||
showAlertDialog(this, {
|
||||
title: "No Home Assistant credentials found.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newUsername = await showPromptDialog(this, {
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.new_username"
|
||||
),
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.change"
|
||||
),
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.caption"
|
||||
),
|
||||
defaultValue: this._params!.entry.username!,
|
||||
});
|
||||
if (newUsername) {
|
||||
try {
|
||||
await adminChangeUsername(
|
||||
this.hass,
|
||||
this._params!.entry.id,
|
||||
newUsername
|
||||
);
|
||||
this._params = {
|
||||
...this._params!,
|
||||
entry: { ...this._params!.entry, username: newUsername },
|
||||
};
|
||||
this._params.replaceEntry(this._params.entry);
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.username_changed"
|
||||
),
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.failed"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _changePassword() {
|
||||
const credential = this._params?.entry.credentials.find(
|
||||
(cred) => cred.type === "homeassistant"
|
||||
|
@@ -182,7 +182,7 @@ export class HaConfigUsers extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
backPath="/config"
|
||||
back-path="/config"
|
||||
.tabs=${configSections.persons}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${this._userData(this._users, this.hass.localize)}
|
||||
@@ -237,6 +237,11 @@ export class HaConfigUsers extends LitElement {
|
||||
|
||||
showUserDetailDialog(this, {
|
||||
entry,
|
||||
replaceEntry: (newEntry: User) => {
|
||||
this._users = this._users!.map((ent) =>
|
||||
ent.id === newEntry.id ? newEntry : ent
|
||||
);
|
||||
},
|
||||
updateEntry: async (values) => {
|
||||
const updated = await updateUser(this.hass!, entry!.id, values);
|
||||
this._users = this._users!.map((ent) =>
|
||||
|
@@ -4,6 +4,7 @@ import { UpdateUserParams, User } from "../../../data/user";
|
||||
export interface UserDetailDialogParams {
|
||||
entry: User;
|
||||
updateEntry: (updates: Partial<UpdateUserParams>) => Promise<unknown>;
|
||||
replaceEntry: (entry: User) => void;
|
||||
removeEntry: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
|
@@ -14,7 +14,7 @@ const SCHEMA = [
|
||||
{
|
||||
name: "location",
|
||||
required: true,
|
||||
selector: { location: { radius: true, radius_readonly: true } },
|
||||
selector: { location: { radius: true } },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -35,6 +35,7 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
this._data = {
|
||||
latitude: this.hass.config.latitude,
|
||||
longitude: this.hass.config.longitude,
|
||||
radius: this.hass.config.radius,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,11 +74,6 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
.computeLabel=${this._computeLabel}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
<p>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.zone.detail.no_edit_home_zone_radius"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@@ -95,7 +91,7 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
location: {
|
||||
latitude: data.latitude,
|
||||
longitude: data.longitude,
|
||||
radius: this.hass.states["zone.home"]?.attributes?.radius || 100,
|
||||
radius: data.radius || 100,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -104,6 +100,7 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
const value = { ...ev.detail.value };
|
||||
value.latitude = value.location.latitude;
|
||||
value.longitude = value.location.longitude;
|
||||
value.radius = value.location.radius;
|
||||
delete value.location;
|
||||
this._data = value;
|
||||
}
|
||||
|
@@ -101,7 +101,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
: zoneRadiusColor,
|
||||
location_editable:
|
||||
entityState.entity_id === "zone.home" && this._canEditCore,
|
||||
radius_editable: false,
|
||||
radius_editable:
|
||||
entityState.entity_id === "zone.home" && this._canEditCore,
|
||||
})
|
||||
);
|
||||
const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({
|
||||
@@ -381,8 +382,14 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _radiusUpdated(ev: CustomEvent) {
|
||||
private async _radiusUpdated(ev: CustomEvent) {
|
||||
this._activeEntry = ev.detail.id;
|
||||
if (ev.detail.id === "zone.home" && this._canEditCore) {
|
||||
await saveCoreConfig(this.hass, {
|
||||
radius: Math.round(ev.detail.radius),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const entry = this._storageItems!.find((item) => item.id === ev.detail.id);
|
||||
if (!entry) {
|
||||
return;
|
||||
@@ -478,6 +485,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
await saveCoreConfig(this.hass, {
|
||||
latitude: values.latitude,
|
||||
longitude: values.longitude,
|
||||
radius: values.radius,
|
||||
});
|
||||
this._zoomZone("zone.home");
|
||||
}
|
||||
|
@@ -282,6 +282,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
{ statistic_id: issue.data.statistic_id }
|
||||
)}`,
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
confirm: async () => {
|
||||
await clearStatistics(this.hass, [issue.data.statistic_id]);
|
||||
this._deletedStatistics.add(issue.data.statistic_id);
|
||||
@@ -314,7 +315,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
break;
|
||||
case "entity_no_longer_recorded":
|
||||
showAlertDialog(this, {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.title"
|
||||
),
|
||||
@@ -335,7 +336,17 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link"
|
||||
)}</a
|
||||
>`,
|
||||
><br /><br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4"
|
||||
)}`,
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
confirm: async () => {
|
||||
await clearStatistics(this.hass, [issue.data.statistic_id]);
|
||||
this._deletedStatistics.add(issue.data.statistic_id);
|
||||
this._validateStatistics();
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "unsupported_state_class":
|
||||
@@ -381,6 +392,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
{ statistic_id: issue.data.statistic_id }
|
||||
)}`,
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
confirm: async () => {
|
||||
await clearStatistics(this.hass, [issue.data.statistic_id]);
|
||||
this._deletedStatistics.add(issue.data.statistic_id);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
export const filterModes = (
|
||||
supportedModes: string[] | undefined,
|
||||
selectedModes: string[] | undefined
|
||||
): string[] =>
|
||||
export const filterModes = <T extends string = string>(
|
||||
supportedModes: T[] | undefined,
|
||||
selectedModes: T[] | undefined
|
||||
): T[] =>
|
||||
selectedModes
|
||||
? selectedModes.filter((mode) => (supportedModes || []).includes(mode))
|
||||
: supportedModes || [];
|
||||
|
@@ -6,7 +6,6 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-control-select";
|
||||
@@ -70,37 +69,18 @@ class HuiAlarmModeCardFeature
|
||||
}
|
||||
}
|
||||
|
||||
private _modes = memoizeOne(
|
||||
(
|
||||
stateObj: AlarmControlPanelEntity,
|
||||
selectedModes: AlarmMode[] | undefined
|
||||
) => {
|
||||
if (!selectedModes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (Object.keys(ALARM_MODES) as AlarmMode[]).filter((mode) => {
|
||||
const feature = ALARM_MODES[mode].feature;
|
||||
return (
|
||||
(!feature || supportsFeature(stateObj, feature)) &&
|
||||
selectedModes.includes(mode)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
private _getCurrentMode(stateObj: AlarmControlPanelEntity) {
|
||||
return this._modes(stateObj, this._config?.modes).find(
|
||||
(mode) => mode === stateObj.state
|
||||
);
|
||||
}
|
||||
private _getCurrentMode = memoizeOne((stateObj: AlarmControlPanelEntity) => {
|
||||
const supportedModes = supportedAlarmModes(stateObj);
|
||||
return supportedModes.find((mode) => mode === stateObj.state);
|
||||
});
|
||||
|
||||
private async _valueChanged(ev: CustomEvent) {
|
||||
if (!this.stateObj) return;
|
||||
const mode = (ev.detail as any).value as AlarmMode;
|
||||
|
||||
if (mode === this.stateObj!.state) return;
|
||||
if (mode === this.stateObj.state) return;
|
||||
|
||||
const oldMode = this._getCurrentMode(this.stateObj!);
|
||||
const oldMode = this._getCurrentMode(this.stateObj);
|
||||
this._currentMode = mode;
|
||||
|
||||
try {
|
||||
@@ -153,6 +133,7 @@ class HuiAlarmModeCardFeature
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<ha-control-select
|
||||
|
@@ -24,7 +24,7 @@ import { calculateStatisticsSumGrowth } from "../../../../data/recorder";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard } from "../../types";
|
||||
import type { EnergyGridGaugeCardConfig } from "../types";
|
||||
import type { EnergyGridNeutralityGaugeCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
|
||||
const LEVELS: LevelDefinition[] = [
|
||||
@@ -39,7 +39,7 @@ class HuiEnergyGridGaugeCard
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: EnergyGridGaugeCardConfig;
|
||||
@state() private _config?: EnergyGridNeutralityGaugeCardConfig;
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
|
||||
@@ -59,7 +59,7 @@ class HuiEnergyGridGaugeCard
|
||||
return 4;
|
||||
}
|
||||
|
||||
public setConfig(config: EnergyGridGaugeCardConfig): void {
|
||||
public setConfig(config: EnergyGridNeutralityGaugeCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { PropertyValueMap, PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
@@ -10,23 +11,41 @@ import {
|
||||
checkConditionsMet,
|
||||
} from "../common/validate-condition";
|
||||
import { createCardElement } from "../create-element/create-card-element";
|
||||
import type { Lovelace, LovelaceCard, LovelaceLayoutOptions } from "../types";
|
||||
import { createErrorCardConfig } from "../create-element/create-element-base";
|
||||
import type { LovelaceCard, LovelaceLayoutOptions } from "../types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"card-visibility-changed": { value: boolean };
|
||||
"card-updated": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hui-card")
|
||||
export class HuiCard extends ReactiveElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@property({ attribute: false }) public isPanel = false;
|
||||
@property({ type: Boolean }) public isPanel = false;
|
||||
|
||||
@state() public _config?: LovelaceCardConfig;
|
||||
set config(config: LovelaceCardConfig | undefined) {
|
||||
if (!config) return;
|
||||
if (config.type !== this._config?.type) {
|
||||
this._buildElement(config);
|
||||
} else if (config !== this.config) {
|
||||
this._element?.setConfig(config);
|
||||
fireEvent(this, "card-updated");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
public get config() {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
private _config?: LovelaceCardConfig;
|
||||
|
||||
private _element?: LovelaceCard;
|
||||
|
||||
@@ -44,7 +63,7 @@ export class HuiCard extends ReactiveElement {
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._listenMediaQueries();
|
||||
this._updateElement();
|
||||
this._updateVisibility();
|
||||
}
|
||||
|
||||
public getCardSize(): number | Promise<number> {
|
||||
@@ -56,7 +75,7 @@ export class HuiCard extends ReactiveElement {
|
||||
}
|
||||
|
||||
public getLayoutOptions(): LovelaceLayoutOptions {
|
||||
const configOptions = this._config?.layout_options ?? {};
|
||||
const configOptions = this.config?.layout_options ?? {};
|
||||
if (this._element) {
|
||||
const cardOptions = this._element.getLayoutOptions?.() ?? {};
|
||||
return {
|
||||
@@ -67,51 +86,84 @@ export class HuiCard extends ReactiveElement {
|
||||
return configOptions;
|
||||
}
|
||||
|
||||
// Public to make demo happy
|
||||
public createElement(config: LovelaceCardConfig) {
|
||||
const element = createCardElement(config) as LovelaceCard;
|
||||
public getElementLayoutOptions(): LovelaceLayoutOptions {
|
||||
return this._element?.getLayoutOptions?.() ?? {};
|
||||
}
|
||||
|
||||
private _createElement(config: LovelaceCardConfig) {
|
||||
const element = createCardElement(config);
|
||||
element.hass = this.hass;
|
||||
element.editMode = this.lovelace?.editMode;
|
||||
element.preview = this.preview;
|
||||
// For backwards compatibility
|
||||
(element as any).editMode = this.preview;
|
||||
// Update element when the visibility of the card changes (e.g. conditional card or filter card)
|
||||
element.addEventListener("card-visibility-changed", (ev) => {
|
||||
element.addEventListener("card-visibility-changed", (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
this._updateElement();
|
||||
this._updateVisibility();
|
||||
});
|
||||
element.addEventListener(
|
||||
"ll-upgrade",
|
||||
(ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "card-updated");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
this._buildElement(config);
|
||||
fireEvent(this, "card-updated");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
return element;
|
||||
}
|
||||
|
||||
public setConfig(config: LovelaceCardConfig): void {
|
||||
if (this._config === config) {
|
||||
return;
|
||||
}
|
||||
this._config = config;
|
||||
this._element = this.createElement(config);
|
||||
private _buildElement(config: LovelaceCardConfig) {
|
||||
this._element = this._createElement(config);
|
||||
|
||||
while (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
this.appendChild(this._element!);
|
||||
this._updateVisibility();
|
||||
}
|
||||
|
||||
protected update(changedProperties: PropertyValues<typeof this>) {
|
||||
super.update(changedProperties);
|
||||
protected update(changedProps: PropertyValues<typeof this>) {
|
||||
super.update(changedProps);
|
||||
|
||||
if (this._element) {
|
||||
if (changedProperties.has("hass")) {
|
||||
this._element.hass = this.hass;
|
||||
if (changedProps.has("hass")) {
|
||||
try {
|
||||
this._element.hass = this.hass;
|
||||
} catch (e: any) {
|
||||
this._buildElement(createErrorCardConfig(e.message, null));
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("lovelace")) {
|
||||
this._element.editMode = this.lovelace?.editMode;
|
||||
if (changedProps.has("preview")) {
|
||||
try {
|
||||
this._element.preview = this.preview;
|
||||
// For backwards compatibility
|
||||
(this._element as any).editMode = this.preview;
|
||||
} catch (e: any) {
|
||||
this._buildElement(createErrorCardConfig(e.message, null));
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("hass") || changedProperties.has("lovelace")) {
|
||||
this._updateElement();
|
||||
}
|
||||
if (changedProperties.has("isPanel")) {
|
||||
if (changedProps.has("isPanel")) {
|
||||
this._element.isPanel = this.isPanel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(
|
||||
changedProps: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
||||
): void {
|
||||
if (changedProps.has("hass") || changedProps.has("preview")) {
|
||||
this._updateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
private _clearMediaQueries() {
|
||||
this._listeners.forEach((unsub) => unsub());
|
||||
this._listeners = [];
|
||||
@@ -119,42 +171,50 @@ export class HuiCard extends ReactiveElement {
|
||||
|
||||
private _listenMediaQueries() {
|
||||
this._clearMediaQueries();
|
||||
if (!this._config?.visibility) {
|
||||
if (!this.config?.visibility) {
|
||||
return;
|
||||
}
|
||||
const conditions = this._config.visibility;
|
||||
const conditions = this.config.visibility;
|
||||
const hasOnlyMediaQuery =
|
||||
conditions.length === 1 &&
|
||||
conditions[0].condition === "screen" &&
|
||||
!!conditions[0].media_query;
|
||||
|
||||
this._listeners = attachConditionMediaQueriesListeners(
|
||||
this._config.visibility,
|
||||
this.config.visibility,
|
||||
(matches) => {
|
||||
this._updateElement(hasOnlyMediaQuery && matches);
|
||||
this._updateVisibility(hasOnlyMediaQuery && matches);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _updateElement(forceVisible?: boolean) {
|
||||
if (!this._element) {
|
||||
private _updateVisibility(forceVisible?: boolean) {
|
||||
if (!this._element || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._element.hidden) {
|
||||
this.style.setProperty("display", "none");
|
||||
this.toggleAttribute("hidden", true);
|
||||
this._setElementVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const visible =
|
||||
forceVisible ||
|
||||
this.lovelace?.editMode ||
|
||||
!this._config?.visibility ||
|
||||
checkConditionsMet(this._config.visibility, this.hass);
|
||||
this.preview ||
|
||||
!this.config?.visibility ||
|
||||
checkConditionsMet(this.config.visibility, this.hass);
|
||||
this._setElementVisibility(visible);
|
||||
}
|
||||
|
||||
private _setElementVisibility(visible: boolean) {
|
||||
if (!this._element) return;
|
||||
|
||||
if (this.hidden !== !visible) {
|
||||
this.style.setProperty("display", visible ? "" : "none");
|
||||
this.toggleAttribute("hidden", !visible);
|
||||
fireEvent(this, "card-visibility-changed", { value: visible });
|
||||
}
|
||||
|
||||
this.style.setProperty("display", visible ? "" : "none");
|
||||
this.toggleAttribute("hidden", !visible);
|
||||
if (!visible && this._element.parentElement) {
|
||||
this.removeChild(this._element);
|
||||
} else if (visible && !this._element.parentElement) {
|
||||
|
@@ -3,7 +3,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { HuiConditionalBase } from "../components/hui-conditional-base";
|
||||
import { createCardElement } from "../create-element/create-card-element";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { ConditionalCardConfig } from "./types";
|
||||
|
||||
@@ -38,30 +37,15 @@ class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard {
|
||||
}
|
||||
|
||||
private _createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = createCardElement(cardConfig) as LovelaceCard;
|
||||
if (this.hass) {
|
||||
element.hass = this.hass;
|
||||
}
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev) => {
|
||||
ev.stopPropagation();
|
||||
this._rebuildCard(cardConfig);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
const element = document.createElement("hui-card");
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
element.config = cardConfig;
|
||||
return element;
|
||||
}
|
||||
|
||||
private _rebuildCard(config: LovelaceCardConfig): void {
|
||||
this._element = this._createCardElement(config);
|
||||
if (this.lastChild) {
|
||||
this.replaceChild(this._element, this.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
protected setVisibility(conditionMet: boolean): void {
|
||||
const visible = this.editMode || conditionMet;
|
||||
const visible = this.preview || conditionMet;
|
||||
const previouslyHidden = this.hidden;
|
||||
super.setVisibility(conditionMet);
|
||||
if (previouslyHidden !== this.hidden) {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
@@ -11,11 +12,10 @@ import {
|
||||
checkConditionsMet,
|
||||
extractConditionEntityIds,
|
||||
} from "../common/validate-condition";
|
||||
import { createCardElement } from "../create-element/create-card-element";
|
||||
import { EntityFilterEntityConfig } from "../entity-rows/types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { HuiCard } from "./hui-card";
|
||||
import { EntityFilterCardConfig } from "./types";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
@customElement("hui-entity-filter-card")
|
||||
export class HuiEntityFilterCard
|
||||
@@ -55,11 +55,11 @@ export class HuiEntityFilterCard
|
||||
|
||||
@property({ type: Boolean }) public isPanel = false;
|
||||
|
||||
@property({ type: Boolean }) public editMode = false;
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@state() private _config?: EntityFilterCardConfig;
|
||||
|
||||
private _element?: LovelaceCard;
|
||||
private _element?: HuiCard;
|
||||
|
||||
private _configEntities?: EntityFilterEntityConfig[];
|
||||
|
||||
@@ -117,7 +117,7 @@ export class HuiEntityFilterCard
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (this._element) {
|
||||
this._element.hass = this.hass;
|
||||
this._element.editMode = this.editMode;
|
||||
this._element.preview = this.preview;
|
||||
this._element.isPanel = this.isPanel;
|
||||
}
|
||||
|
||||
@@ -173,12 +173,12 @@ export class HuiEntityFilterCard
|
||||
}
|
||||
|
||||
if (!this.lastChild) {
|
||||
this._element.setConfig({
|
||||
this._element.config = {
|
||||
...this._baseCardConfig!,
|
||||
entities: entitiesList,
|
||||
});
|
||||
};
|
||||
this._oldEntities = entitiesList;
|
||||
} else if (this._element.tagName !== "HUI-ERROR-CARD") {
|
||||
} else {
|
||||
const isSame =
|
||||
this._oldEntities &&
|
||||
entitiesList.length === this._oldEntities.length &&
|
||||
@@ -186,10 +186,10 @@ export class HuiEntityFilterCard
|
||||
|
||||
if (!isSame) {
|
||||
this._oldEntities = entitiesList;
|
||||
this._element.setConfig({
|
||||
this._element.config = {
|
||||
...this._baseCardConfig!,
|
||||
entities: entitiesList,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,33 +245,12 @@ export class HuiEntityFilterCard
|
||||
}
|
||||
|
||||
private _createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = createCardElement(cardConfig) as LovelaceCard;
|
||||
if (this.hass) {
|
||||
element.hass = this.hass;
|
||||
}
|
||||
element.isPanel = this.isPanel;
|
||||
element.editMode = this.editMode;
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev) => {
|
||||
ev.stopPropagation();
|
||||
this._rebuildCard(element, cardConfig);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
const element = document.createElement("hui-card");
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
element.config = cardConfig;
|
||||
return element;
|
||||
}
|
||||
|
||||
private _rebuildCard(
|
||||
cardElToReplace: LovelaceCard,
|
||||
config: LovelaceCardConfig
|
||||
): void {
|
||||
const newCardEl = this._createCardElement(config);
|
||||
if (cardElToReplace.parentElement) {
|
||||
cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace);
|
||||
}
|
||||
this._element = newCardEl;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { dump } from "js-yaml";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
@@ -10,6 +10,8 @@ import { ErrorCardConfig } from "./types";
|
||||
export class HuiErrorCard extends LitElement implements LovelaceCard {
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public preview = false;
|
||||
|
||||
@state() private _config?: ErrorCardConfig;
|
||||
|
||||
public getCardSize(): number {
|
||||
|
@@ -38,7 +38,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public editMode = false;
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@state() private _config?: MarkdownCardConfig;
|
||||
|
||||
@@ -163,12 +163,12 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
user: this.hass.user!.name,
|
||||
},
|
||||
strict: true,
|
||||
report_errors: this.editMode,
|
||||
report_errors: this.preview,
|
||||
}
|
||||
);
|
||||
await this._unsubRenderTemplate;
|
||||
} catch (e: any) {
|
||||
if (this.editMode) {
|
||||
if (this.preview) {
|
||||
this._error = e.message;
|
||||
this._errorLevel = undefined;
|
||||
}
|
||||
|
@@ -1,19 +1,12 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { createCardElement } from "../create-element/create-card-element";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import "./hui-card";
|
||||
import type { HuiCard } from "./hui-card";
|
||||
import { StackCardConfig } from "./types";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
|
||||
export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
extends LitElement
|
||||
@@ -30,9 +23,9 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public editMode = false;
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@state() protected _cards?: LovelaceCard[];
|
||||
@state() protected _cards?: HuiCard[];
|
||||
|
||||
@state() protected _config?: T;
|
||||
|
||||
@@ -49,30 +42,36 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
}
|
||||
this._config = config;
|
||||
this._cards = config.cards.map((card) => {
|
||||
const element = this._createCardElement(card) as LovelaceCard;
|
||||
const element = this._createCardElement(card);
|
||||
return element;
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (
|
||||
!this._cards ||
|
||||
(!changedProps.has("hass") && !changedProps.has("editMode"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
protected update(changedProperties) {
|
||||
super.update(changedProperties);
|
||||
|
||||
for (const element of this._cards) {
|
||||
if (this.hass) {
|
||||
element.hass = this.hass;
|
||||
if (this._cards) {
|
||||
if (changedProperties.has("hass")) {
|
||||
this._cards.forEach((card) => {
|
||||
card.hass = this.hass;
|
||||
});
|
||||
}
|
||||
if (this.editMode !== undefined) {
|
||||
element.editMode = this.editMode;
|
||||
if (changedProperties.has("editMode")) {
|
||||
this._cards.forEach((card) => {
|
||||
card.preview = this.preview;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = document.createElement("hui-card");
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
element.config = cardConfig;
|
||||
return element;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this._cards) {
|
||||
return nothing;
|
||||
@@ -110,34 +109,4 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = createCardElement(cardConfig) as LovelaceCard;
|
||||
if (this.hass) {
|
||||
element.hass = this.hass;
|
||||
}
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev) => {
|
||||
ev.stopPropagation();
|
||||
this._rebuildCard(element, cardConfig);
|
||||
fireEvent(this, "ll-rebuild");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
return element;
|
||||
}
|
||||
|
||||
private _rebuildCard(
|
||||
cardElToReplace: LovelaceCard,
|
||||
config: LovelaceCardConfig
|
||||
): void {
|
||||
const newCardEl = this._createCardElement(config);
|
||||
if (cardElToReplace.parentElement) {
|
||||
cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace);
|
||||
}
|
||||
this._cards = this._cards!.map((curCardEl) =>
|
||||
curCardEl === cardElToReplace ? newCardEl : curCardEl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -26,6 +26,7 @@ import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-checkbox";
|
||||
@@ -243,7 +244,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
${this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.MOVE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-button-menu>
|
||||
? html`<ha-button-menu @closed=${stopPropagation}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
@@ -287,7 +288,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
${this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-button-menu>
|
||||
? html`<ha-button-menu @closed=${stopPropagation}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
|
@@ -118,85 +118,74 @@ export interface EnergyCardBaseConfig extends LovelaceCardConfig {
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergySummaryCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergySummaryCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-summary";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergyDistributionCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyDistributionCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-distribution";
|
||||
title?: string;
|
||||
link_dashboard?: boolean;
|
||||
collection_key?: string;
|
||||
}
|
||||
export interface EnergyUsageGraphCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyUsageGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-usage-graph";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergySolarGraphCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergySolarGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-solar-graph";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergyGasGraphCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyGasGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-gas-graph";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergyWaterGraphCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyWaterGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-water-graph";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyDevicesGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-devices-graph";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
max_devices?: number;
|
||||
}
|
||||
|
||||
export interface EnergyDevicesDetailGraphCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyDevicesDetailGraphCardConfig
|
||||
extends EnergyCardBaseConfig {
|
||||
type: "energy-devices-detail-graph";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
max_devices?: number;
|
||||
}
|
||||
|
||||
export interface EnergySourcesTableCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergySourcesTableCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-sources-table";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergySolarGaugeCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-solar-consumed-gauge";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergySelfSufficiencyGaugeCardConfig
|
||||
extends LovelaceCardConfig {
|
||||
extends EnergyCardBaseConfig {
|
||||
type: "energy-self-sufficiency-gauge";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergyGridGaugeCardConfig extends LovelaceCardConfig {
|
||||
type: "energy-grid-result-gauge";
|
||||
export interface EnergyGridNeutralityGaugeCardConfig
|
||||
extends EnergyCardBaseConfig {
|
||||
type: "energy-grid-neutrality-gauge";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyCarbonGaugeCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-carbon-consumed-gauge";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EntityFilterCardConfig extends LovelaceCardConfig {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { SENSOR_ENTITIES } from "../../../common/const";
|
||||
import { SENSOR_ENTITIES, ASSIST_ENTITIES } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
@@ -35,17 +35,16 @@ import { ButtonsHeaderFooterConfig } from "../header-footer/types";
|
||||
const HIDE_DOMAIN = new Set([
|
||||
"automation",
|
||||
"configurator",
|
||||
"conversation",
|
||||
"device_tracker",
|
||||
"event",
|
||||
"geo_location",
|
||||
"notify",
|
||||
"persistent_notification",
|
||||
"script",
|
||||
"sun",
|
||||
"zone",
|
||||
"event",
|
||||
"tts",
|
||||
"stt",
|
||||
"todo",
|
||||
"zone",
|
||||
...ASSIST_ENTITIES,
|
||||
]);
|
||||
|
||||
const HIDE_PLATFORM = new Set(["mobile_app"]);
|
||||
|
@@ -353,7 +353,7 @@ export class HuiCardOptions extends LitElement {
|
||||
allowDashboardChange: true,
|
||||
header: this.hass!.localize("ui.panel.lovelace.editor.move_card.header"),
|
||||
viewSelectedCallback: async (urlPath, selectedDashConfig, viewIndex) => {
|
||||
const view = this.lovelace!.config.views[viewIndex];
|
||||
const view = selectedDashConfig.views[viewIndex];
|
||||
|
||||
if (!isStrategyView(view) && view.type === SECTION_VIEW_LAYOUT) {
|
||||
showAlertDialog(this, {
|
||||
|
@@ -3,6 +3,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { HuiCard } from "../cards/hui-card";
|
||||
import { ConditionalCardConfig } from "../cards/types";
|
||||
import {
|
||||
Condition,
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
validateConditionalConfig,
|
||||
} from "../common/validate-condition";
|
||||
import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types";
|
||||
import { LovelaceCard } from "../types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -24,11 +24,11 @@ declare global {
|
||||
export class HuiConditionalBase extends ReactiveElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public editMode = false;
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@state() protected _config?: ConditionalCardConfig | ConditionalRowConfig;
|
||||
|
||||
protected _element?: LovelaceCard | LovelaceRow;
|
||||
protected _element?: HuiCard | LovelaceRow;
|
||||
|
||||
private _listeners: MediaQueriesListener[] = [];
|
||||
|
||||
@@ -116,7 +116,7 @@ export class HuiConditionalBase extends ReactiveElement {
|
||||
changed.has("_element") ||
|
||||
changed.has("_config") ||
|
||||
changed.has("hass") ||
|
||||
changed.has("editMode")
|
||||
changed.has("preview")
|
||||
) {
|
||||
this._listenMediaQueries();
|
||||
this._updateVisibility();
|
||||
@@ -128,7 +128,7 @@ export class HuiConditionalBase extends ReactiveElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._element.editMode = this.editMode;
|
||||
this._element.preview = this.preview;
|
||||
|
||||
const conditionMet = checkConditionsMet(
|
||||
this._config!.conditions,
|
||||
@@ -142,7 +142,7 @@ export class HuiConditionalBase extends ReactiveElement {
|
||||
if (!this._element || !this.hass) {
|
||||
return;
|
||||
}
|
||||
const visible = this.editMode || conditionMet;
|
||||
const visible = this.preview || conditionMet;
|
||||
if (this.hidden !== !visible) {
|
||||
this.toggleAttribute("hidden", !visible);
|
||||
this.style.setProperty("display", visible ? "" : "none");
|
||||
|
@@ -152,6 +152,7 @@ const _lazyCreate = <T extends keyof CreateElementConfigTypes>(
|
||||
customElements.whenDefined(tag).then(() => {
|
||||
try {
|
||||
customElements.upgrade(element);
|
||||
fireEvent(element, "ll-upgrade");
|
||||
// @ts-ignore
|
||||
element.setConfig(config);
|
||||
} catch (err: any) {
|
||||
|
389
src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts
Normal file
389
src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts
Normal file
@@ -0,0 +1,389 @@
|
||||
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"slider-moved": { value?: number };
|
||||
}
|
||||
}
|
||||
|
||||
const A11Y_KEY_CODES = new Set([
|
||||
"ArrowRight",
|
||||
"ArrowUp",
|
||||
"ArrowLeft",
|
||||
"ArrowDown",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"Home",
|
||||
"End",
|
||||
]);
|
||||
|
||||
@customElement("ha-grid-layout-slider")
|
||||
export class HaGridLayoutSlider extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public vertical = false;
|
||||
|
||||
@property({ attribute: "touch-action" })
|
||||
public touchAction?: string;
|
||||
|
||||
@property({ type: Number })
|
||||
public value?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public step = 1;
|
||||
|
||||
@property({ type: Number })
|
||||
public min = 1;
|
||||
|
||||
@property({ type: Number })
|
||||
public max = 4;
|
||||
|
||||
@property({ type: Number })
|
||||
public range?: number;
|
||||
|
||||
@state()
|
||||
public pressed = false;
|
||||
|
||||
private _mc?: HammerManager;
|
||||
|
||||
private get _range() {
|
||||
return this.range ?? this.max;
|
||||
}
|
||||
|
||||
private _valueToPercentage(value: number) {
|
||||
const percentage = this._boundedValue(value) / this._range;
|
||||
return percentage;
|
||||
}
|
||||
|
||||
private _percentageToValue(percentage: number) {
|
||||
return this._range * percentage;
|
||||
}
|
||||
|
||||
private _steppedValue(value: number) {
|
||||
return Math.round(value / this.step) * this.step;
|
||||
}
|
||||
|
||||
private _boundedValue(value: number) {
|
||||
return Math.min(Math.max(value, this.min), this.max);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.setupListeners();
|
||||
this.setAttribute("role", "slider");
|
||||
if (!this.hasAttribute("tabindex")) {
|
||||
this.setAttribute("tabindex", "0");
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("value")) {
|
||||
const valuenow = this._steppedValue(this.value ?? 0);
|
||||
this.setAttribute("aria-valuenow", valuenow.toString());
|
||||
this.setAttribute("aria-valuetext", valuenow.toString());
|
||||
}
|
||||
if (changedProps.has("min")) {
|
||||
this.setAttribute("aria-valuemin", this.min.toString());
|
||||
}
|
||||
if (changedProps.has("max")) {
|
||||
this.setAttribute("aria-valuemax", this.max.toString());
|
||||
}
|
||||
if (changedProps.has("vertical")) {
|
||||
const orientation = this.vertical ? "vertical" : "horizontal";
|
||||
this.setAttribute("aria-orientation", orientation);
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.destroyListeners();
|
||||
}
|
||||
|
||||
@query("#slider")
|
||||
private slider;
|
||||
|
||||
setupListeners() {
|
||||
if (this.slider && !this._mc) {
|
||||
this._mc = new Manager(this.slider, {
|
||||
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
|
||||
});
|
||||
this._mc.add(
|
||||
new Pan({
|
||||
threshold: 10,
|
||||
direction: DIRECTION_ALL,
|
||||
enable: true,
|
||||
})
|
||||
);
|
||||
|
||||
this._mc.add(new Tap({ event: "singletap" }));
|
||||
|
||||
let savedValue;
|
||||
this._mc.on("panstart", () => {
|
||||
if (this.disabled) return;
|
||||
this.pressed = true;
|
||||
savedValue = this.value;
|
||||
});
|
||||
this._mc.on("pancancel", () => {
|
||||
if (this.disabled) return;
|
||||
this.pressed = false;
|
||||
this.value = savedValue;
|
||||
});
|
||||
this._mc.on("panmove", (e) => {
|
||||
if (this.disabled) return;
|
||||
const percentage = this._getPercentageFromEvent(e);
|
||||
this.value = this._percentageToValue(percentage);
|
||||
const value = this._steppedValue(this._boundedValue(this.value));
|
||||
fireEvent(this, "slider-moved", { value });
|
||||
});
|
||||
this._mc.on("panend", (e) => {
|
||||
if (this.disabled) return;
|
||||
this.pressed = false;
|
||||
const percentage = this._getPercentageFromEvent(e);
|
||||
const value = this._percentageToValue(percentage);
|
||||
this.value = this._steppedValue(this._boundedValue(value));
|
||||
fireEvent(this, "slider-moved", { value: undefined });
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
});
|
||||
|
||||
this._mc.on("singletap", (e) => {
|
||||
if (this.disabled) return;
|
||||
const percentage = this._getPercentageFromEvent(e);
|
||||
const value = this._percentageToValue(percentage);
|
||||
this.value = this._steppedValue(this._boundedValue(value));
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
});
|
||||
|
||||
this.addEventListener("keydown", this._handleKeyDown);
|
||||
this.addEventListener("keyup", this._handleKeyUp);
|
||||
}
|
||||
}
|
||||
|
||||
destroyListeners() {
|
||||
if (this._mc) {
|
||||
this._mc.destroy();
|
||||
this._mc = undefined;
|
||||
}
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
this.removeEventListener("keyup", this._handleKeyUp);
|
||||
}
|
||||
|
||||
private get _tenPercentStep() {
|
||||
return Math.max(this.step, (this.max - this.min) / 10);
|
||||
}
|
||||
|
||||
_handleKeyDown(e: KeyboardEvent) {
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
e.preventDefault();
|
||||
switch (e.code) {
|
||||
case "ArrowRight":
|
||||
case "ArrowUp":
|
||||
this.value = this._boundedValue((this.value ?? 0) + this.step);
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
case "ArrowDown":
|
||||
this.value = this._boundedValue((this.value ?? 0) - this.step);
|
||||
break;
|
||||
case "PageUp":
|
||||
this.value = this._steppedValue(
|
||||
this._boundedValue((this.value ?? 0) + this._tenPercentStep)
|
||||
);
|
||||
break;
|
||||
case "PageDown":
|
||||
this.value = this._steppedValue(
|
||||
this._boundedValue((this.value ?? 0) - this._tenPercentStep)
|
||||
);
|
||||
break;
|
||||
case "Home":
|
||||
this.value = this.min;
|
||||
break;
|
||||
case "End":
|
||||
this.value = this.max;
|
||||
break;
|
||||
}
|
||||
fireEvent(this, "slider-moved", { value: this.value });
|
||||
}
|
||||
|
||||
_handleKeyUp(e: KeyboardEvent) {
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
e.preventDefault();
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
|
||||
private _getPercentageFromEvent = (e: HammerInput) => {
|
||||
if (this.vertical) {
|
||||
const y = e.center.y;
|
||||
const offset = e.target.getBoundingClientRect().top;
|
||||
const total = e.target.clientHeight;
|
||||
return Math.max(Math.min(1, (y - offset) / total), 0);
|
||||
}
|
||||
const x = e.center.x;
|
||||
const offset = e.target.getBoundingClientRect().left;
|
||||
const total = e.target.clientWidth;
|
||||
return Math.max(Math.min(1, (x - offset) / total), 0);
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
class="container${classMap({
|
||||
pressed: this.pressed,
|
||||
})}"
|
||||
style=${styleMap({
|
||||
"--value": `${this._valueToPercentage(this.value ?? 0)}`,
|
||||
})}
|
||||
>
|
||||
<div id="slider" class="slider">
|
||||
<div class="track">
|
||||
<div class="background">
|
||||
<div
|
||||
class="active"
|
||||
style=${styleMap({
|
||||
"--min": `${this.min / this._range}`,
|
||||
"--max": `${1 - this.max / this._range}`,
|
||||
})}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
${this.value !== undefined
|
||||
? html`<div class="handle"></div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
--grid-layout-slider: 48px;
|
||||
height: var(--grid-layout-slider);
|
||||
width: 100%;
|
||||
outline: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
}
|
||||
:host(:focus-visible) {
|
||||
box-shadow: 0 0 0 2px var(--primary-color);
|
||||
}
|
||||
:host([vertical]) {
|
||||
width: var(--grid-layout-slider);
|
||||
height: 100%;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.slider {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transform: translateZ(0);
|
||||
overflow: visible;
|
||||
cursor: pointer;
|
||||
}
|
||||
.slider * {
|
||||
pointer-events: none;
|
||||
}
|
||||
.track {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
height: 16px;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
:host([vertical]) .track {
|
||||
width: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
.background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--disabled-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.active {
|
||||
position: absolute;
|
||||
background: grey;
|
||||
top: 0;
|
||||
right: calc(var(--max) * 100%);
|
||||
bottom: 0;
|
||||
left: calc(var(--min) * 100%);
|
||||
}
|
||||
:host([vertical]) .active {
|
||||
top: calc(var(--min) * 100%);
|
||||
right: 0;
|
||||
bottom: calc(var(--max) * 100%);
|
||||
left: 0;
|
||||
}
|
||||
.handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 16px;
|
||||
transform: translate(-50%, 0);
|
||||
background: var(--card-background-color);
|
||||
left: calc(var(--value, 0%) * 100%);
|
||||
transition:
|
||||
left 180ms ease-in-out,
|
||||
top 180ms ease-in-out;
|
||||
}
|
||||
:host([vertical]) .handle {
|
||||
transform: translate(0, -50%);
|
||||
left: 0;
|
||||
top: calc(var(--value, 0%) * 100%);
|
||||
height: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
.handle::after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 4px;
|
||||
border-radius: 2px;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
background: grey;
|
||||
content: "";
|
||||
}
|
||||
:host([vertical]) .handle::after {
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
:host(:disabled) .slider {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.pressed .handle {
|
||||
transition: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-grid-layout-slider": HaGridLayoutSlider;
|
||||
}
|
||||
}
|
@@ -6,17 +6,21 @@ import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import { getCardElementClass } from "../../create-element/create-card-element";
|
||||
import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
|
||||
import { HuiElementEditor } from "../hui-element-editor";
|
||||
import "./hui-card-layout-editor";
|
||||
import "./hui-card-visibility-editor";
|
||||
|
||||
const TABS = ["config", "visibility"] as const;
|
||||
type Tab = "config" | "visibility" | "layout";
|
||||
|
||||
@customElement("hui-card-element-editor")
|
||||
export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
|
||||
@state() private _curTab: (typeof TABS)[number] = TABS[0];
|
||||
@state() private _curTab: Tab = "config";
|
||||
|
||||
@property({ type: Boolean, attribute: "show-visibility-tab" })
|
||||
public showVisibilityTab = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-layout-tab" })
|
||||
public showLayoutTab = false;
|
||||
|
||||
protected async getConfigElement(): Promise<LovelaceCardEditor | undefined> {
|
||||
const elClass = await getCardElementClass(this.configElementType!);
|
||||
|
||||
@@ -52,7 +56,11 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
|
||||
}
|
||||
|
||||
protected renderConfigElement(): TemplateResult {
|
||||
if (!this.showVisibilityTab) return super.renderConfigElement();
|
||||
const displayedTabs: Tab[] = ["config"];
|
||||
if (this.showVisibilityTab) displayedTabs.push("visibility");
|
||||
if (this.showLayoutTab) displayedTabs.push("layout");
|
||||
|
||||
if (displayedTabs.length === 1) return super.renderConfigElement();
|
||||
|
||||
let content: TemplateResult<1> | typeof nothing = nothing;
|
||||
|
||||
@@ -69,19 +77,28 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
|
||||
></hui-card-visibility-editor>
|
||||
`;
|
||||
break;
|
||||
case "layout":
|
||||
content = html`
|
||||
<hui-card-layout-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this.value}
|
||||
@value-changed=${this._configChanged}
|
||||
>
|
||||
</hui-card-layout-editor>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<paper-tabs
|
||||
scrollable
|
||||
hide-scroll-buttons
|
||||
.selected=${TABS.indexOf(this._curTab)}
|
||||
.selected=${displayedTabs.indexOf(this._curTab)}
|
||||
@selected-item-changed=${this._handleTabSelected}
|
||||
>
|
||||
${TABS.map(
|
||||
${displayedTabs.map(
|
||||
(tab, index) => html`
|
||||
<paper-tab id=${tab} .dialogInitialFocus=${index === 0}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_card.tab-${tab}`
|
||||
`ui.panel.lovelace.editor.edit_card.tab_${tab}`
|
||||
)}
|
||||
</paper-tab>
|
||||
`
|
||||
|
266
src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts
Normal file
266
src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiCheck, mdiDotsVertical } from "@mdi/js";
|
||||
import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-grid-size-picker";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-slider";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { HuiCard } from "../../cards/hui-card";
|
||||
import { DEFAULT_GRID_OPTIONS } from "../../sections/hui-grid-section";
|
||||
import { LovelaceLayoutOptions } from "../../types";
|
||||
|
||||
@customElement("hui-card-layout-editor")
|
||||
export class HuiCardLayoutEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config!: LovelaceCardConfig;
|
||||
|
||||
@state() _defaultLayoutOptions?: LovelaceLayoutOptions;
|
||||
|
||||
@state() public _yamlMode = false;
|
||||
|
||||
@state() public _uiAvailable = true;
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
private _cardElement?: HuiCard;
|
||||
|
||||
private _gridSizeValue = memoizeOne(
|
||||
(
|
||||
options?: LovelaceLayoutOptions,
|
||||
defaultOptions?: LovelaceLayoutOptions
|
||||
) => ({
|
||||
rows:
|
||||
options?.grid_rows ??
|
||||
defaultOptions?.grid_rows ??
|
||||
DEFAULT_GRID_OPTIONS.grid_rows,
|
||||
columns:
|
||||
options?.grid_columns ??
|
||||
defaultOptions?.grid_columns ??
|
||||
DEFAULT_GRID_OPTIONS.grid_columns,
|
||||
})
|
||||
);
|
||||
|
||||
private _isDefault = memoizeOne(
|
||||
(options?: LovelaceLayoutOptions) =>
|
||||
options?.grid_columns === undefined && options?.grid_rows === undefined
|
||||
);
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="header">
|
||||
<p class="intro">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_card.layout.explanation`
|
||||
)}
|
||||
</p>
|
||||
<ha-button-menu
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
.corner=${"BOTTOM_END"}
|
||||
.menuCorner=${"END"}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
>
|
||||
</ha-icon-button>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${!this._uiAvailable}>
|
||||
${this.hass.localize("ui.panel.lovelace.editor.edit_card.edit_ui")}
|
||||
${!this._yamlMode
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="selected_menu_item"
|
||||
slot="graphic"
|
||||
.path=${mdiCheck}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
</ha-list-item>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.edit_yaml"
|
||||
)}
|
||||
${this._yamlMode
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="selected_menu_item"
|
||||
slot="graphic"
|
||||
.path=${mdiCheck}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
${this._yamlMode
|
||||
? html`
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this.config.layout_options}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
<ha-grid-size-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._gridSizeValue(
|
||||
this.config.layout_options,
|
||||
this._defaultLayoutOptions
|
||||
)}
|
||||
.isDefault=${this._isDefault(this.config.layout_options)}
|
||||
@value-changed=${this._gridSizeChanged}
|
||||
></ha-grid-size-picker>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProps);
|
||||
try {
|
||||
this._cardElement = document.createElement("hui-card");
|
||||
this._cardElement.hass = this.hass;
|
||||
this._cardElement.preview = true;
|
||||
this._cardElement.config = this.config;
|
||||
this._cardElement.addEventListener("card-updated", (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
this._defaultLayoutOptions =
|
||||
this._cardElement?.getElementLayoutOptions();
|
||||
});
|
||||
this._defaultLayoutOptions = this._cardElement.getElementLayoutOptions();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>): void {
|
||||
super.updated(changedProps);
|
||||
if (this._cardElement) {
|
||||
if (changedProps.has("hass")) {
|
||||
this._cardElement.hass = this.hass;
|
||||
}
|
||||
if (changedProps.has("config")) {
|
||||
this._cardElement.config = this.config;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._yamlMode = false;
|
||||
break;
|
||||
case 1:
|
||||
this._yamlMode = true;
|
||||
break;
|
||||
case 2:
|
||||
this._reset();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _reset() {
|
||||
const newConfig = { ...this.config };
|
||||
delete newConfig.layout_options;
|
||||
this._yamlEditor?.setValue({});
|
||||
fireEvent(this, "value-changed", { value: newConfig });
|
||||
}
|
||||
|
||||
private _gridSizeChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
const newConfig: LovelaceCardConfig = {
|
||||
...this.config,
|
||||
layout_options: {
|
||||
...this.config.layout_options,
|
||||
grid_columns: value.columns,
|
||||
grid_rows: value.rows,
|
||||
},
|
||||
};
|
||||
|
||||
if (newConfig.layout_options!.grid_columns === undefined) {
|
||||
delete newConfig.layout_options!.grid_columns;
|
||||
}
|
||||
if (newConfig.layout_options!.grid_rows === undefined) {
|
||||
delete newConfig.layout_options!.grid_rows;
|
||||
}
|
||||
if (Object.keys(newConfig.layout_options!).length === 0) {
|
||||
delete newConfig.layout_options;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: newConfig });
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const options = ev.detail.value as LovelaceLayoutOptions;
|
||||
const newConfig: LovelaceCardConfig = {
|
||||
...this.config,
|
||||
layout_options: options,
|
||||
};
|
||||
fireEvent(this, "value-changed", { value: newConfig });
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.header .intro {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.header ha-button-menu {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
margin-top: -8px;
|
||||
}
|
||||
.selected_menu_item {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-grid-size-picker {
|
||||
display: block;
|
||||
max-width: 250px;
|
||||
margin: 16px auto;
|
||||
}
|
||||
ha-yaml-editor {
|
||||
display: block;
|
||||
margin: 16px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-card-layout-editor": HuiCardLayoutEditor;
|
||||
}
|
||||
}
|
@@ -1,106 +0,0 @@
|
||||
import { PropertyValues, ReactiveElement } from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { createCardElement } from "../../create-element/create-card-element";
|
||||
import { createErrorCardConfig } from "../../create-element/create-element-base";
|
||||
import { LovelaceCard } from "../../types";
|
||||
|
||||
export class HuiCardPreview extends ReactiveElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceCardConfig;
|
||||
|
||||
private _element?: LovelaceCard;
|
||||
|
||||
private get _error() {
|
||||
return this._element?.tagName === "HUI-ERROR-CARD";
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener("ll-rebuild", () => {
|
||||
this._cleanup();
|
||||
if (this.config) {
|
||||
this._createCard(this.config);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
protected update(changedProperties: PropertyValues) {
|
||||
super.update(changedProperties);
|
||||
|
||||
if (changedProperties.has("config")) {
|
||||
const oldConfig = changedProperties.get("config") as
|
||||
| undefined
|
||||
| LovelaceCardConfig;
|
||||
|
||||
if (!this.config) {
|
||||
this._cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.config.type) {
|
||||
this._createCard(
|
||||
createErrorCardConfig("No card type found", this.config)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._element) {
|
||||
this._createCard(this.config);
|
||||
return;
|
||||
}
|
||||
|
||||
// in case the element was an error element we always want to recreate it
|
||||
if (!this._error && oldConfig && this.config.type === oldConfig.type) {
|
||||
try {
|
||||
this._element.setConfig(this.config);
|
||||
} catch (err: any) {
|
||||
this._createCard(createErrorCardConfig(err.message, this.config));
|
||||
}
|
||||
} else {
|
||||
this._createCard(this.config);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has("hass")) {
|
||||
if (this._element) {
|
||||
this._element.hass = this.hass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _createCard(configValue: LovelaceCardConfig): void {
|
||||
this._cleanup();
|
||||
this._element = createCardElement(configValue);
|
||||
|
||||
this._element.editMode = true;
|
||||
|
||||
if (this.hass) {
|
||||
this._element!.hass = this.hass;
|
||||
}
|
||||
|
||||
this.appendChild(this._element!);
|
||||
}
|
||||
|
||||
private _cleanup() {
|
||||
if (!this._element) {
|
||||
return;
|
||||
}
|
||||
this.removeChild(this._element);
|
||||
this._element = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-card-preview": HuiCardPreview;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-card-preview", HuiCardPreview);
|
@@ -1,4 +1,4 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
@@ -16,11 +16,11 @@ export class HuiCardVisibilityEditor extends LitElement {
|
||||
render() {
|
||||
const conditions = this.config.visibility ?? [];
|
||||
return html`
|
||||
<ha-alert alert-type="info">
|
||||
<p class="intro">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_card.visibility.explanation`
|
||||
)}
|
||||
</ha-alert>
|
||||
</p>
|
||||
<ha-card-conditions-editor
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
@@ -42,6 +42,14 @@ export class HuiCardVisibilityEditor extends LitElement {
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: newConfig });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.intro {
|
||||
margin: 0;
|
||||
color: var(--secondary-text-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -5,7 +5,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./hui-card-preview";
|
||||
import "../../cards/hui-card";
|
||||
import type { DeleteCardDialogParams } from "./show-delete-card-dialog";
|
||||
|
||||
@customElement("hui-dialog-delete-card")
|
||||
@@ -45,10 +45,11 @@ export class HuiDialogDeleteCard extends LitElement {
|
||||
${this._cardConfig
|
||||
? html`
|
||||
<div class="element-preview">
|
||||
<hui-card-preview
|
||||
<hui-card
|
||||
.hass=${this.hass}
|
||||
.config=${this._cardConfig}
|
||||
></hui-card-preview>
|
||||
preview
|
||||
></hui-card>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
@@ -74,7 +75,7 @@ export class HuiDialogDeleteCard extends LitElement {
|
||||
.element-preview {
|
||||
position: relative;
|
||||
}
|
||||
hui-card-preview {
|
||||
hui-card {
|
||||
margin: 4px auto;
|
||||
max-width: 500px;
|
||||
display: block;
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
@@ -29,6 +30,7 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../sections/hui-section";
|
||||
import { addCard, replaceCard } from "../config-util";
|
||||
import { getCardDocumentationURL } from "../get-card-documentation-url";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
@@ -36,7 +38,7 @@ import { findLovelaceContainer } from "../lovelace-path";
|
||||
import type { GUIModeChangedEvent } from "../types";
|
||||
import "./hui-card-element-editor";
|
||||
import type { HuiCardElementEditor } from "./hui-card-element-editor";
|
||||
import "./hui-card-preview";
|
||||
import "../../cards/hui-card";
|
||||
import type { EditCardDialogParams } from "./show-edit-card-dialog";
|
||||
|
||||
declare global {
|
||||
@@ -234,6 +236,7 @@ export class HuiDialogEditCard
|
||||
<div class="content">
|
||||
<div class="element-editor">
|
||||
<hui-card-element-editor
|
||||
.showLayoutTab=${this._shouldShowLayoutTab()}
|
||||
.showVisibilityTab=${this._cardConfig?.type !== "conditional"}
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
@@ -245,11 +248,23 @@ export class HuiDialogEditCard
|
||||
></hui-card-element-editor>
|
||||
</div>
|
||||
<div class="element-preview">
|
||||
<hui-card-preview
|
||||
.hass=${this.hass}
|
||||
.config=${this._cardConfig}
|
||||
class=${this._error ? "blur" : ""}
|
||||
></hui-card-preview>
|
||||
${this._isInSection
|
||||
? html`
|
||||
<hui-section
|
||||
.hass=${this.hass}
|
||||
.config=${this._cardConfigInSection(this._cardConfig)}
|
||||
preview
|
||||
class=${this._error ? "blur" : ""}
|
||||
></hui-section>
|
||||
`
|
||||
: html`
|
||||
<hui-card
|
||||
.hass=${this.hass}
|
||||
.config=${this._cardConfig}
|
||||
preview
|
||||
class=${this._error ? "blur" : ""}
|
||||
></hui-card>
|
||||
`}
|
||||
${this._error
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
@@ -334,6 +349,34 @@ export class HuiDialogEditCard
|
||||
this._cardEditorEl?.focusYamlEditor();
|
||||
}
|
||||
|
||||
private get _isInSection() {
|
||||
return this._params!.path.length === 2;
|
||||
}
|
||||
|
||||
private _shouldShowLayoutTab(): boolean {
|
||||
/**
|
||||
* Only show layout tab for cards in a grid section
|
||||
* In the future, every section and view should be able to bring their own editor for layout.
|
||||
* For now, we limit it to grid sections as it's the only section type
|
||||
* */
|
||||
return (
|
||||
this._isInSection &&
|
||||
(!this._containerConfig.type || this._containerConfig.type === "grid")
|
||||
);
|
||||
}
|
||||
|
||||
private _cardConfigInSection = memoizeOne(
|
||||
(cardConfig?: LovelaceCardConfig) => {
|
||||
const { cards, title, ...containerConfig } = this
|
||||
._containerConfig as LovelaceSectionConfig;
|
||||
|
||||
return {
|
||||
...containerConfig,
|
||||
cards: cardConfig ? [cardConfig] : [],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
private get _canSave(): boolean {
|
||||
if (this._saving) {
|
||||
return false;
|
||||
@@ -452,10 +495,18 @@ export class HuiDialogEditCard
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content hui-card-preview {
|
||||
margin: 4px auto;
|
||||
.content hui-card {
|
||||
display: block;
|
||||
padding: 4px;
|
||||
margin: 0 auto;
|
||||
max-width: 390px;
|
||||
}
|
||||
.content hui-section {
|
||||
display: block;
|
||||
padding: 4px;
|
||||
margin: 0 auto;
|
||||
max-width: var(--ha-view-sections-column-max-width, 500px);
|
||||
}
|
||||
.content .element-editor {
|
||||
margin: 0 10px;
|
||||
}
|
||||
@@ -470,11 +521,16 @@ export class HuiDialogEditCard
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.content hui-card-preview {
|
||||
.content hui-card {
|
||||
padding: 8px 10px;
|
||||
margin: auto 0px;
|
||||
max-width: 500px;
|
||||
}
|
||||
.content hui-section {
|
||||
padding: 8px 10px;
|
||||
margin: auto 0px;
|
||||
max-width: var(--ha-view-sections-column-max-width, 500px);
|
||||
}
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
@@ -498,7 +554,7 @@ export class HuiDialogEditCard
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
hui-card-preview {
|
||||
hui-card {
|
||||
padding-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
|
@@ -18,7 +18,7 @@ import {
|
||||
LovelaceContainerPath,
|
||||
parseLovelaceContainerPath,
|
||||
} from "../lovelace-path";
|
||||
import "./hui-card-preview";
|
||||
import "../../cards/hui-card";
|
||||
import { showCreateCardDialog } from "./show-create-card-dialog";
|
||||
import { SuggestCardDialogParams } from "./show-suggest-card-dialog";
|
||||
|
||||
@@ -75,6 +75,7 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||
<hui-section
|
||||
.hass=${this.hass}
|
||||
.config=${this._sectionConfig}
|
||||
preview
|
||||
></hui-section>
|
||||
</div>
|
||||
`;
|
||||
@@ -84,10 +85,11 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||
<div class="element-preview">
|
||||
${this._cardConfig.map(
|
||||
(cardConfig) => html`
|
||||
<hui-card-preview
|
||||
<hui-card
|
||||
.hass=${this.hass}
|
||||
.config=${cardConfig}
|
||||
></hui-card-preview>
|
||||
preview
|
||||
></hui-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@@ -191,7 +193,7 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||
.element-preview {
|
||||
position: relative;
|
||||
}
|
||||
hui-card-preview,
|
||||
hui-card,
|
||||
hui-section {
|
||||
padding-top: 8px;
|
||||
margin: 4px auto;
|
||||
|
@@ -1,104 +0,0 @@
|
||||
import { PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { LovelaceSectionElement } from "../../../../data/lovelace";
|
||||
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { createSectionElement } from "../../create-element/create-section-element";
|
||||
import { createErrorSectionConfig } from "../../sections/hui-error-section";
|
||||
import { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
|
||||
@customElement("hui-section-preview")
|
||||
export class HuiSectionPreview extends ReactiveElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceSectionConfig;
|
||||
|
||||
private _element?: LovelaceSectionElement;
|
||||
|
||||
private get _error() {
|
||||
return this._element?.tagName === "HUI-ERROR-SECTION";
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener("ll-rebuild", () => {
|
||||
this._cleanup();
|
||||
if (this.config) {
|
||||
this._createSection(this.config);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
protected update(changedProperties: PropertyValues) {
|
||||
super.update(changedProperties);
|
||||
|
||||
if (changedProperties.has("config")) {
|
||||
const oldConfig = changedProperties.get("config") as
|
||||
| undefined
|
||||
| LovelaceSectionConfig;
|
||||
|
||||
if (!this.config) {
|
||||
this._cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.config.type) {
|
||||
this._createSection(createErrorSectionConfig("No section type found"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._element) {
|
||||
this._createSection(this.config);
|
||||
return;
|
||||
}
|
||||
|
||||
// in case the element was an error element we always want to recreate it
|
||||
if (!this._error && oldConfig && this.config.type === oldConfig.type) {
|
||||
try {
|
||||
this._element.setConfig(this.config);
|
||||
} catch (err: any) {
|
||||
this._createSection(createErrorSectionConfig(err.message));
|
||||
}
|
||||
} else {
|
||||
this._createSection(this.config);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has("hass")) {
|
||||
if (this._element) {
|
||||
this._element.hass = this.hass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _createSection(configValue: LovelaceSectionConfig): void {
|
||||
this._cleanup();
|
||||
this._element = createSectionElement(configValue) as LovelaceSectionElement;
|
||||
|
||||
if (this.hass) {
|
||||
this._element!.hass = this.hass;
|
||||
}
|
||||
|
||||
this.appendChild(this._element!);
|
||||
}
|
||||
|
||||
private _cleanup() {
|
||||
if (!this._element) {
|
||||
return;
|
||||
}
|
||||
this.removeChild(this._element);
|
||||
this._element = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-section-preview": HuiSectionPreview;
|
||||
}
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
import { preventDefault } from "@fullcalendar/core/internal";
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiCheck, mdiDelete, mdiDotsVertical, mdiFlask } from "@mdi/js";
|
||||
import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
||||
@@ -6,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-alert";
|
||||
|
@@ -205,7 +205,7 @@ export class HuiDialogEditSection
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
${content}
|
||||
<ha-button slot="secondaryAction">
|
||||
<ha-button slot="secondaryAction" @click=${this._cancel}>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
|
||||
|
@@ -81,7 +81,7 @@ export type LovelaceRowConfig =
|
||||
|
||||
export interface LovelaceRow extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
editMode?: boolean;
|
||||
preview?: boolean;
|
||||
setConfig(config: LovelaceRowConfig);
|
||||
}
|
||||
|
||||
|
@@ -11,10 +11,10 @@ import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { HuiCard } from "../cards/hui-card";
|
||||
import "../components/hui-card-edit-mode";
|
||||
import { moveCard } from "../editor/config-util";
|
||||
import type { Lovelace } from "../types";
|
||||
import { HuiCard } from "../cards/hui-card";
|
||||
import type { Lovelace, LovelaceLayoutOptions } from "../types";
|
||||
|
||||
const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
|
||||
delay: 100,
|
||||
@@ -23,6 +23,11 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
|
||||
invertedSwapThreshold: 0.7,
|
||||
} as HaSortableOptions;
|
||||
|
||||
export const DEFAULT_GRID_OPTIONS: LovelaceLayoutOptions = {
|
||||
grid_columns: 4,
|
||||
grid_rows: 1,
|
||||
};
|
||||
|
||||
export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -95,11 +100,15 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
const card = this.cards![idx];
|
||||
const layoutOptions = card.getLayoutOptions();
|
||||
|
||||
const columnSize =
|
||||
layoutOptions.grid_columns ?? DEFAULT_GRID_OPTIONS.grid_columns;
|
||||
const rowSize =
|
||||
layoutOptions.grid_rows ?? DEFAULT_GRID_OPTIONS.grid_rows;
|
||||
return html`
|
||||
<div
|
||||
style=${styleMap({
|
||||
"--column-size": layoutOptions.grid_columns,
|
||||
"--row-size": layoutOptions.grid_rows,
|
||||
"--column-size": columnSize,
|
||||
"--row-size": rowSize,
|
||||
})}
|
||||
class="card ${classMap({
|
||||
"fit-rows": typeof layoutOptions?.grid_rows === "number",
|
||||
@@ -202,6 +211,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
margin: 0px;
|
||||
letter-spacing: 0.1px;
|
||||
line-height: 32px;
|
||||
text-align: var(--ha-view-sections-title-text-align, start);
|
||||
min-height: 32px;
|
||||
display: block;
|
||||
padding: 24px 10px 10px;
|
||||
@@ -215,8 +225,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
.card {
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
position: relative;
|
||||
grid-row: span var(--row-size, 1);
|
||||
grid-column: span var(--column-size, 4);
|
||||
grid-row: span var(--row-size);
|
||||
grid-column: span var(--column-size);
|
||||
}
|
||||
|
||||
.card.fit-rows {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceSectionElement } from "../../../data/lovelace";
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
attachConditionMediaQueriesListeners,
|
||||
checkConditionsMet,
|
||||
} from "../common/validate-condition";
|
||||
import { createErrorCardConfig } from "../create-element/create-element-base";
|
||||
import { createSectionElement } from "../create-element/create-section-element";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
@@ -26,7 +26,6 @@ import { parseLovelaceCardPath } from "../editor/lovelace-path";
|
||||
import { generateLovelaceSectionStrategy } from "../strategies/get-strategy";
|
||||
import type { Lovelace } from "../types";
|
||||
import { DEFAULT_SECTION_LAYOUT } from "./const";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -42,6 +41,8 @@ export class HuiSection extends ReactiveElement {
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public preview = false;
|
||||
|
||||
@property({ type: Number }) public index!: number;
|
||||
|
||||
@property({ type: Number }) public viewIndex!: number;
|
||||
@@ -54,23 +55,15 @@ export class HuiSection extends ReactiveElement {
|
||||
|
||||
private _listeners: MediaQueriesListener[] = [];
|
||||
|
||||
// Public to make demo happy
|
||||
public createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
private _createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = document.createElement("hui-card");
|
||||
element.hass = this.hass;
|
||||
element.lovelace = this.lovelace;
|
||||
element.setConfig(cardConfig);
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev: Event) => {
|
||||
// In edit mode let it go to hui-root and rebuild whole section.
|
||||
if (!this.lovelace!.editMode) {
|
||||
ev.stopPropagation();
|
||||
this._rebuildCard(element, cardConfig);
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
element.preview = this.preview;
|
||||
element.config = cardConfig;
|
||||
element.addEventListener("card-updated", (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
this._cards = [...this._cards];
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -121,28 +114,23 @@ export class HuiSection extends ReactiveElement {
|
||||
// Config has not changed. Just props
|
||||
if (changedProperties.has("hass")) {
|
||||
this._cards.forEach((element) => {
|
||||
try {
|
||||
element.hass = this.hass;
|
||||
} catch (e: any) {
|
||||
this._rebuildCard(element, createErrorCardConfig(e.message, null));
|
||||
}
|
||||
element.hass = this.hass;
|
||||
});
|
||||
this._layoutElement.hass = this.hass;
|
||||
}
|
||||
if (changedProperties.has("lovelace")) {
|
||||
this._layoutElement.lovelace = this.lovelace;
|
||||
}
|
||||
if (changedProperties.has("preview")) {
|
||||
this._layoutElement.preview = this.preview;
|
||||
this._cards.forEach((element) => {
|
||||
try {
|
||||
element.lovelace = this.lovelace;
|
||||
} catch (e: any) {
|
||||
this._rebuildCard(element, createErrorCardConfig(e.message, null));
|
||||
}
|
||||
element.preview = this.preview;
|
||||
});
|
||||
}
|
||||
if (changedProperties.has("_cards")) {
|
||||
this._layoutElement.cards = this._cards;
|
||||
}
|
||||
if (changedProperties.has("hass") || changedProperties.has("lovelace")) {
|
||||
if (changedProperties.has("hass") || changedProperties.has("preview")) {
|
||||
this._updateElement();
|
||||
}
|
||||
}
|
||||
@@ -222,7 +210,7 @@ export class HuiSection extends ReactiveElement {
|
||||
}
|
||||
const visible =
|
||||
forceVisible ||
|
||||
this.lovelace?.editMode ||
|
||||
this.preview ||
|
||||
!this.config.visibility ||
|
||||
checkConditionsMet(this.config.visibility, this.hass);
|
||||
|
||||
@@ -283,22 +271,8 @@ export class HuiSection extends ReactiveElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._cards = config.cards.map((cardConfig) => {
|
||||
const element = this.createCardElement(cardConfig);
|
||||
return element;
|
||||
});
|
||||
}
|
||||
|
||||
private _rebuildCard(
|
||||
cardElToReplace: HuiCard,
|
||||
config: LovelaceCardConfig
|
||||
): void {
|
||||
const newCardEl = this.createCardElement(config);
|
||||
if (cardElToReplace.parentElement) {
|
||||
cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace);
|
||||
}
|
||||
this._cards = this._cards!.map((curCardEl) =>
|
||||
curCardEl === cardElToReplace ? newCardEl : curCardEl
|
||||
this._cards = config.cards.map((cardConfig) =>
|
||||
this._createCardElement(cardConfig)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ declare global {
|
||||
// eslint-disable-next-line
|
||||
interface HASSDomEvents {
|
||||
"ll-rebuild": Record<string, unknown>;
|
||||
"ll-upgrade": Record<string, unknown>;
|
||||
"ll-badge-rebuild": Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
@@ -47,7 +48,7 @@ export type LovelaceLayoutOptions = {
|
||||
export interface LovelaceCard extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
isPanel?: boolean;
|
||||
editMode?: boolean;
|
||||
preview?: boolean;
|
||||
getCardSize(): number | Promise<number>;
|
||||
getLayoutOptions?(): LovelaceLayoutOptions;
|
||||
setConfig(config: LovelaceCardConfig): void;
|
||||
|
@@ -17,7 +17,7 @@ import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { HuiCard } from "../cards/hui-card";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
|
||||
import type { Lovelace, LovelaceBadge } from "../types";
|
||||
|
||||
// Find column with < 5 size, else smallest column
|
||||
const getColumnIndex = (columnSizes: number[], size: number) => {
|
||||
@@ -248,17 +248,17 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _addCardToColumn(columnEl, index, editMode) {
|
||||
const card: LovelaceCard = this.cards[index];
|
||||
if (!editMode || this.isStrategy) {
|
||||
card.editMode = false;
|
||||
private _addCardToColumn(columnEl, index, preview) {
|
||||
const card: HuiCard = this.cards[index];
|
||||
if (!preview || this.isStrategy) {
|
||||
card.preview = false;
|
||||
columnEl.appendChild(card);
|
||||
} else {
|
||||
const wrapper = document.createElement("hui-card-options");
|
||||
wrapper.hass = this.hass;
|
||||
wrapper.lovelace = this.lovelace;
|
||||
wrapper.path = [this.index!, index];
|
||||
card.editMode = true;
|
||||
card.preview = true;
|
||||
wrapper.appendChild(card);
|
||||
columnEl.appendChild(wrapper);
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { HuiCard } from "../cards/hui-card";
|
||||
import { HuiCardOptions } from "../components/hui-card-options";
|
||||
import { HuiWarning } from "../components/hui-warning";
|
||||
import type { Lovelace, LovelaceCard } from "../types";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
let editCodeLoaded = false;
|
||||
|
||||
@@ -32,7 +32,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@property({ attribute: false }) public cards: HuiCard[] = [];
|
||||
|
||||
@state() private _card?: LovelaceCard | HuiWarning | HuiCardOptions;
|
||||
@state() private _card?: HuiCard | HuiWarning | HuiCardOptions;
|
||||
|
||||
public setConfig(_config: LovelaceViewConfig): void {}
|
||||
|
||||
@@ -104,11 +104,11 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const card: LovelaceCard = this.cards[0];
|
||||
const card: HuiCard = this.cards[0];
|
||||
card.isPanel = true;
|
||||
|
||||
if (this.isStrategy || !this.lovelace?.editMode) {
|
||||
card.editMode = false;
|
||||
card.preview = false;
|
||||
this._card = card;
|
||||
return;
|
||||
}
|
||||
@@ -118,7 +118,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
||||
wrapper.lovelace = this.lovelace;
|
||||
wrapper.path = [this.index!, 0];
|
||||
wrapper.hidePosition = true;
|
||||
card.editMode = true;
|
||||
card.preview = true;
|
||||
wrapper.appendChild(card);
|
||||
this._card = wrapper;
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { HuiCard } from "../cards/hui-card";
|
||||
import { HuiCardOptions } from "../components/hui-card-options";
|
||||
import { replaceCard } from "../editor/config-util";
|
||||
import type { Lovelace, LovelaceCard } from "../types";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -140,18 +140,18 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
});
|
||||
}
|
||||
|
||||
this.cards.forEach((card: LovelaceCard, idx) => {
|
||||
this.cards.forEach((card, idx) => {
|
||||
const cardConfig = this._config?.cards?.[idx];
|
||||
let element: LovelaceCard | HuiCardOptions;
|
||||
let element: HuiCard | HuiCardOptions;
|
||||
if (this.isStrategy || !this.lovelace?.editMode) {
|
||||
card.editMode = false;
|
||||
card.preview = false;
|
||||
element = card;
|
||||
} else {
|
||||
element = document.createElement("hui-card-options");
|
||||
element.hass = this.hass;
|
||||
element.lovelace = this.lovelace;
|
||||
element.path = [this.index!, idx];
|
||||
card.editMode = true;
|
||||
card.preview = true;
|
||||
const movePositionButton = document.createElement("ha-icon-button");
|
||||
movePositionButton.slot = "buttons";
|
||||
const moveIcon = document.createElement("ha-svg-icon");
|
||||
|
@@ -21,7 +21,6 @@ import "../cards/hui-card";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import { createBadgeElement } from "../create-element/create-badge-element";
|
||||
import { createErrorCardConfig } from "../create-element/create-element-base";
|
||||
import { createViewElement } from "../create-element/create-view-element";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
@@ -77,19 +76,12 @@ export class HUIView extends ReactiveElement {
|
||||
private _createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = document.createElement("hui-card");
|
||||
element.hass = this.hass;
|
||||
element.lovelace = this.lovelace;
|
||||
element.setConfig(cardConfig);
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev: Event) => {
|
||||
// In edit mode let it go to hui-root and rebuild whole view.
|
||||
if (!this.lovelace!.editMode) {
|
||||
ev.stopPropagation();
|
||||
this._rebuildCard(element, cardConfig);
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
element.preview = this.lovelace.editMode;
|
||||
element.config = cardConfig;
|
||||
element.addEventListener("card-updated", (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
this._cards = [...this._cards];
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -117,6 +109,7 @@ export class HUIView extends ReactiveElement {
|
||||
element.lovelace = this.lovelace;
|
||||
element.config = sectionConfig;
|
||||
element.viewIndex = this.index;
|
||||
element.preview = this.lovelace.editMode;
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev: Event) => {
|
||||
@@ -183,11 +176,7 @@ export class HUIView extends ReactiveElement {
|
||||
});
|
||||
|
||||
this._cards.forEach((element) => {
|
||||
try {
|
||||
element.hass = this.hass;
|
||||
} catch (e: any) {
|
||||
this._rebuildCard(element, createErrorCardConfig(e.message, null));
|
||||
}
|
||||
element.hass = this.hass;
|
||||
});
|
||||
|
||||
this._sections.forEach((element) => {
|
||||
@@ -221,17 +210,13 @@ export class HUIView extends ReactiveElement {
|
||||
try {
|
||||
element.hass = this.hass;
|
||||
element.lovelace = this.lovelace;
|
||||
element.preview = this.lovelace.editMode;
|
||||
} catch (e: any) {
|
||||
this._rebuildSection(element, createErrorSectionConfig(e.message));
|
||||
}
|
||||
});
|
||||
this._cards.forEach((element) => {
|
||||
try {
|
||||
element.hass = this.hass;
|
||||
element.lovelace = this.lovelace;
|
||||
} catch (e: any) {
|
||||
this._rebuildCard(element, createErrorCardConfig(e.message, null));
|
||||
}
|
||||
element.preview = this.lovelace.editMode;
|
||||
});
|
||||
}
|
||||
if (changedProperties.has("_cards")) {
|
||||
@@ -388,19 +373,6 @@ export class HUIView extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _rebuildCard(
|
||||
cardElToReplace: HuiCard,
|
||||
config: LovelaceCardConfig
|
||||
): void {
|
||||
const newCardEl = this._createCardElement(config);
|
||||
if (cardElToReplace.parentElement) {
|
||||
cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace);
|
||||
}
|
||||
this._cards = this._cards!.map((curCardEl) =>
|
||||
curCardEl === cardElToReplace ? newCardEl : curCardEl
|
||||
);
|
||||
}
|
||||
|
||||
private _rebuildBadge(
|
||||
badgeElToReplace: LovelaceBadge,
|
||||
config: LovelaceBadgeConfig
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user