Compare commits

..

74 Commits

Author SHA1 Message Date
Paul Bottein
ebe5207b6e Improve datatable 2024-06-25 17:05:51 +02:00
Paul Bottein
bd1ede4145 Fix grid size picker size (#21161) 2024-06-25 12:24:04 +02:00
Paul Bottein
321a085c0e Resize card editor (#21115) 2024-06-24 22:10:31 +02:00
Bram Kragten
6a3041988a Allow to change username (#21152) 2024-06-24 18:51:43 +02:00
renovate[bot]
23fcdf876c Update dependency @codemirror/view to v6.28.2 (#21154)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-24 12:24:01 -04:00
Bram Kragten
00f325e961 Support expandable in initial form data (#21153) 2024-06-24 17:30:43 +02:00
renovate[bot]
d00b3cfc61 Update Yarn to v4.3.1 (#21149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-24 13:33:19 +02:00
karwosts
4cc9e74ea8 Fix 'Move to view' operation (#21142) 2024-06-24 11:56:06 +02:00
dependabot[bot]
a56b9a96ce Bump softprops/action-gh-release from 2.0.5 to 2.0.6 (#21148)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 08:37:48 +02:00
renovate[bot]
d4b5f4bc14 Update dependency @octokit/rest to v21 (#21146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-23 23:22:08 -04:00
renovate[bot]
cf1523ee73 Update dependency @types/chromecast-caf-receiver to v6.0.15 (#21138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-23 12:54:15 +02:00
renovate[bot]
f5d571ca84 Update dependency webpack to v5.92.1 (#21134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-22 20:54:24 +02:00
karwosts
362e92f313 Add some weather attribute icons and units (#21133) 2024-06-22 15:42:26 +02:00
Jan-Philipp Benecke
5ddf72b973 Add preview to Threshold config & option flow (#19845)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2024-06-22 13:32:40 +02:00
renovate[bot]
6e78c28f51 Update dependency @codemirror/autocomplete to v6.16.3 (#21130)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-22 09:10:52 +02:00
renovate[bot]
772f0bb669 Update dependency tar to v7.4.0 (#21129)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-22 08:51:43 +02:00
renovate[bot]
846c2a848f Update dependency glob to v10.4.2 (#21127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-22 08:51:08 +02:00
renovate[bot]
8495757005 Update dependency @braintree/sanitize-url to v7.0.3 (#21126) 2024-06-21 22:09:25 -04:00
karwosts
9960d38b91 Offer to delete no-longer-recorded statistics (#21119) 2024-06-21 11:18:24 +02:00
Simon Lamon
d3222f8bb0 Various fixes in dialogs (#20935)
* allow escape and scrim action for repair dialogs

* improve delete entity dialogs

* reiterate refresh token dialog wordings (kept refresh token for now)

* improve device delete dialogs

* Improve deletable text and invalidation
2024-06-21 11:15:01 +02:00
Simon Lamon
2e5cce5409 Replace paper-listbox in cast frontend (#19954)
* hc-cast

* Update cast/src/launcher/layout/hc-cast.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Update cast/src/launcher/layout/hc-cast.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Update cast/src/launcher/layout/hc-cast.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Fixes

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-06-21 11:13:13 +02:00
Paul Bottein
f78946447f Show card preview inside section in card editor (#21065)
* Show card inside section in card editor

* Replace edit mode by preview

* Add backward compatibility for custom cards

* Re-order props
2024-06-21 11:12:18 +02:00
Jay Turner
eb0579ddc5 Use EnergyCardBaseConfig where appropriate (#20896)
* Use EnergyCardBaseConfig where appropriate

* Update type key

* Rename class

* Run prettier
2024-06-21 11:11:41 +02:00
Steve Repsher
686424fc70 Add CoreJS polyfills to modern build (#20676) 2024-06-21 11:07:39 +02:00
renovate[bot]
039e9b40bd Update typescript-eslint monorepo to v7.13.1 (#21121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-20 14:41:07 -04:00
karwosts
8272bef890 Sort filter-domains on translated name (#21116) 2024-06-19 17:31:04 +02:00
Kevin Jahrens
62528b2413 Fix back paths (#21112)
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2024-06-19 07:08:14 +00:00
renovate[bot]
fa24f529e0 Lock file maintenance (#21114)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-19 08:58:38 +02:00
dependabot[bot]
43a54f6cda Bump ws from 7.5.9 to 7.5.10 (#21111) 2024-06-18 20:56:53 -04:00
Paulus Schoutsen
9c153bbd58 Split out service entities (#21076)
* Hide notify entiites from generated dashboard

* Split out service entities on device info page

* Update src/panels/lovelace/common/generate-lovelace-config.ts

* Split service -> notify/assist
2024-06-18 08:58:45 +02:00
Bram Kragten
27afe9ecb7 Wrap code editor for template selector (#21104)
* Wrap code editor for template selector
2024-06-17 19:15:55 +00:00
dependabot[bot]
72f989e2bd Bump actions/checkout from 4.1.6 to 4.1.7 (#21102) 2024-06-17 10:19:48 +02:00
renovate[bot]
a6ef46565f Update dependency @codemirror/view to v6.28.1 (#21099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-16 09:43:11 +02:00
renovate[bot]
a35ac09688 Update dependency lint-staged to v15.2.7 (#21098)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-15 20:55:19 +02:00
Douwe
27024135ea Update hui-grid-section.ts (#21088)
Add variable to align title
2024-06-15 18:21:42 +02:00
Franck Nijhof
8759ed740a Make the radius of the home zone configurable (#21096) 2024-06-15 14:41:09 +02:00
Franck Nijhof
bb3e8ae33d Update home-assistant-js-websocket to 9.4.0 (#21097) 2024-06-15 08:03:55 -04:00
renovate[bot]
b5b60c9bf0 Update vaadinWebComponents monorepo to v24.4.0 (#21089)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-15 00:12:37 -04:00
renovate[bot]
3b6a2cf7d8 Update dependency @codemirror/view to v6.28.0 (#21081)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-15 00:09:09 -04:00
renovate[bot]
7e10e14102 Update dependency lint-staged to v15.2.6 (#21092)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-14 21:21:25 +02:00
renovate[bot]
a580abab4a Update dependency webpack to v5.92.0 (#21090)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-14 20:21:11 +02:00
renovate[bot]
11523c08c4 Update Yarn to v4.3.0 (#21084) 2024-06-14 12:29:02 -04:00
renovate[bot]
7a8988528b Update dependency prettier to v3.3.2 (#21087)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-14 08:42:55 +02:00
renovate[bot]
2a6380f083 Update typescript-eslint monorepo to v7.13.0 (#21082)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-14 08:29:55 +02:00
Paul Bottein
29881c8bb4 Fix selected state for selected config entries (#21079) 2024-06-13 18:26:45 +02:00
Simon Lamon
56254ddf03 Fix diagnostic download not downloading (#21078) 2024-06-13 13:31:30 +02:00
Paulus Schoutsen
007ba70641 Hide notify entities from generated dashboard (#21075)
Hide notify entiites from generated dashboard
2024-06-13 08:04:27 +02:00
Steve Repsher
3e1227b064 Do not inject Intl polyfill into ecma402-abstract package (#21074) 2024-06-12 14:45:57 -04:00
Matthias Alphart
067e179f26 Translation support for device automation extra fields (#20567)
* Translation support for device trigger extra fields

* Prefer component translation over default

* Move device trigger extra_fields translations to backend

* move translations for extra_fields of conditions and actions too
2024-06-12 14:09:50 +02:00
AlCalzone
9a3f7df25e Z-Wave: Prevent closing the Add Device dialog when user input is required (#20999)
* prevent closing the Z-Wave Add node dialog when user input is required

* ask user for confirmation before leaving page during bootstrapping

* fix: no non-null assertion
2024-06-12 13:48:40 +02:00
Paul Bottein
c7b4e8f37c Fix current mode not selected in card feature (#21063) 2024-06-12 13:40:44 +02:00
Paul Bottein
bfa8b886ab Make update actions sticky on more info (#21053) 2024-06-12 13:39:04 +02:00
Paul Bottein
433c00b73a Move card loading logic into hui-card (#21018)
* Move card rebuild to hui-card

* Use hui card in stack card

* add once to event

* Do not use state

* Use hui card in conditional card

* Use editMode instead of lovelace in hui card

* Fix edit mode

* Use hui-card in card dialog and panel todo

* Fix edit mode

* Fix types

* Migrate entity filter card

* Update demo card

* Fix UI view

* Allow edit mode attribute

* Remove unused condition

* Remove unused section preview code

* Remove useless check for config
2024-06-12 13:38:21 +02:00
Stefan Agner
a497f42f73 Move send credentials to phone to main Thread configuration panel (#21066)
Move send credentials to phone to main view

Currently the button is hidden in the more info dialog, and even there
it seems that it is currently not rendered correctly.

This moves the button to the main view, make it more obvious while
still keeping it out of the way if the feature is not applicable.
2024-06-12 13:35:29 +02:00
Stefan Agner
165723cb5b Clarify Thread credentials transfer direction (#21067)
"Import credentials" on a phone can be missunderstood as importing
credentials from Home Assistant to the phone, but this is not what
this command is doing.

Use "Send credentials to Home Assistant" to make it clear what the
direction of the transfer is.
2024-06-12 13:32:51 +02:00
Bruno Pantaleão Gonçalves
42b5fa696a Fix "canImportKeychain" boolean for thread panel (#21062) 2024-06-11 10:04:13 +02:00
renovate[bot]
59062d96a8 Update dependency @rollup/plugin-commonjs to v26 (#21037)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-10 21:56:01 +02:00
Paulus Schoutsen
d36bbfe07d Hide hidden updates from sidebar/settings (#21058) 2024-06-10 15:52:24 -04:00
Bram Kragten
0d489213a4 Bumped version to 20240610.0 2024-06-10 19:50:10 +02:00
koostamas
c54acc9369 Fix automation describeCondition for number state type (#21052)
When using a number state condition in an automation, the UI used an incorrect evaluation when trying to describe the condition which made the label default to the default value.
To fix this, I just changed the evaluation to check directly for `undefined` value.
2024-06-10 19:19:26 +02:00
Simon Lamon
562bc084f0 Revert fullcalendar back to v6.1.11 (#21039)
* Drop fullcalendar back to v6.1.11

* Add resolution
2024-06-10 09:23:53 +02:00
karwosts
6fce2f35a5 Add a title to triggered dialog (#21046) 2024-06-09 21:33:58 +02:00
karwosts
f4e24bed2e Fix cancel button in section edit (#21045) 2024-06-09 12:54:38 +00:00
renovate[bot]
09969c0e2d Update babel monorepo to v7.24.7 (#21035)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-08 19:52:09 +02:00
renovate[bot]
4b0181774b Update CodeMirror (#21022)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-08 19:51:08 +02:00
renovate[bot]
272db5e9e8 Update dependency @rollup/plugin-replace to v5.0.7 (#21036)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-08 19:50:43 +02:00
renovate[bot]
9ae3a824d9 Update dependency prettier to v3.3.1 (#21033)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-08 12:29:12 +02:00
renovate[bot]
9db55c9391 Update dependency @lit-labs/virtualizer to v2.0.13 (#21029)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-08 09:42:52 +02:00
renovate[bot]
59697127c0 Update dependency @rollup/plugin-replace to v5.0.6 (#21031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-08 08:50:00 +02:00
renovate[bot]
565600e945 Update dependency @codemirror/view to v6.26.4 (#21021)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-07 16:08:38 +02:00
karwosts
721eebf367 Add floor selector to script fields (#21016) 2024-06-07 13:02:58 +02:00
renovate[bot]
f5ae842167 Update typescript-eslint monorepo to v7.12.0 (#21014) 2024-06-06 20:35:13 -04:00
renovate[bot]
3575734ed0 Update dependency @codemirror/language to v6.10.2 (#21008) 2024-06-06 20:34:16 -04:00
karwosts
4e8de1f64d Todo button menu: add stop propagation (#20996) 2024-06-05 11:43:21 +02:00
107 changed files with 3769 additions and 2688 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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 :-(");
}
});
};
});

View File

@@ -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;
}

View File

@@ -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"
}

View File

@@ -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"

View File

@@ -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

View File

@@ -0,0 +1 @@
export const preventDefault = (ev) => ev.preventDefault();

View File

@@ -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),
];

View File

@@ -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) {

View File

@@ -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;

View 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;
}
}

View File

@@ -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,

View File

@@ -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>`

View File

@@ -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++;

View File

@@ -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,

View File

@@ -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`,
{

View File

@@ -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;

View File

@@ -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

View File

@@ -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[];

View File

@@ -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
View 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,
});

View File

@@ -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 "%";

View File

@@ -14,6 +14,7 @@ export interface Zone {
export interface HomeZoneMutableParams {
latitude: number;
longitude: number;
radius: number;
}
export interface ZoneMutableParams {

View 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;
}
}

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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;

View File

@@ -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(

View File

@@ -5,6 +5,7 @@ export const demoConfig: HassConfig = {
elevation: 300,
latitude: 52.3731339,
longitude: 4.8903147,
radius: 100,
unit_system: {
length: "km",
mass: "kg",

View File

@@ -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(

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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),

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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)

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 });

View File

@@ -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 {

View File

@@ -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, {

View File

@@ -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 {

View File

@@ -31,8 +31,6 @@ class DialogIntegrationStartup extends LitElement {
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
hideActions
.heading=${createCloseHeading(
this.hass,

View File

@@ -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")

View File

@@ -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"

View File

@@ -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) =>

View File

@@ -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>;
}

View File

@@ -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;
}

View File

@@ -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");
}

View File

@@ -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);

View File

@@ -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 || [];

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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
);
}
}

View File

@@ -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}

View File

@@ -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 {

View File

@@ -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"]);

View File

@@ -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, {

View File

@@ -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");

View File

@@ -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) {

View 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;
}
}

View File

@@ -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>
`

View 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;
}
}

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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";

View File

@@ -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>

View File

@@ -81,7 +81,7 @@ export type LovelaceRowConfig =
export interface LovelaceRow extends HTMLElement {
hass?: HomeAssistant;
editMode?: boolean;
preview?: boolean;
setConfig(config: LovelaceRowConfig);
}

View File

@@ -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 {

View File

@@ -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)
);
}
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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");

View File

@@ -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