mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-21 23:18:11 +00:00
Compare commits
192 Commits
master
...
navigation
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06cdef459d | ||
|
|
5f1f537fbd | ||
|
|
6024cacc0c | ||
|
|
bca8cd013a | ||
|
|
e04034f9a0 | ||
|
|
122d607f3c | ||
|
|
11b0708cc1 | ||
|
|
51b1042dec | ||
|
|
44632d043a | ||
|
|
10631e4c5b | ||
|
|
5c701a6cf6 | ||
|
|
611886916e | ||
|
|
a642cc7dae | ||
|
|
2d496afdbc | ||
|
|
681b60614f | ||
|
|
1654a67d30 | ||
|
|
8f00494d53 | ||
|
|
d9c7c0422b | ||
|
|
2d24447c3c | ||
|
|
3b8d485ec6 | ||
|
|
4e4a00e3e9 | ||
|
|
14f7328f92 | ||
|
|
c5ad074dfb | ||
|
|
07aa8706ce | ||
|
|
1665fa3775 | ||
|
|
3be6a87658 | ||
|
|
9092de5c28 | ||
|
|
e0fc661920 | ||
|
|
aaad8e5434 | ||
|
|
35c668744a | ||
|
|
081b0a0222 | ||
|
|
829cd96e9b | ||
|
|
c404e66ee5 | ||
|
|
d47d3f9694 | ||
|
|
622df52167 | ||
|
|
20345c3771 | ||
|
|
fc7468a43b | ||
|
|
c8ab65cde9 | ||
|
|
001ade24ea | ||
|
|
f987cfe91e | ||
|
|
0bc0acebe0 | ||
|
|
2087efca51 | ||
|
|
16a4a07080 | ||
|
|
58eefcb216 | ||
|
|
92a36ac687 | ||
|
|
8221ca8971 | ||
|
|
8f69cbb6c1 | ||
|
|
4c111e1a7d | ||
|
|
af8659d8ed | ||
|
|
c9e1c9e0a3 | ||
|
|
9d15499953 | ||
|
|
7d54dd4940 | ||
|
|
8ef717df6e | ||
|
|
7b4a7403c8 | ||
|
|
22b7c52828 | ||
|
|
04dbeb5e84 | ||
|
|
db5f823b6b | ||
|
|
1d241aa49a | ||
|
|
fece231faf | ||
|
|
fffb3c3a28 | ||
|
|
fe14d436ff | ||
|
|
42e02be928 | ||
|
|
6213b6cd2a | ||
|
|
cd75c55392 | ||
|
|
ca325020d7 | ||
|
|
6250402661 | ||
|
|
0bfca79851 | ||
|
|
49bddf6139 | ||
|
|
0daf94e98f | ||
|
|
00a3237611 | ||
|
|
53deb3f419 | ||
|
|
6c1c7cead3 | ||
|
|
f8d65cc0ec | ||
|
|
5be7bad176 | ||
|
|
0a54a93a39 | ||
|
|
156583aff1 | ||
|
|
7572257821 | ||
|
|
4703cf802f | ||
|
|
55c2315329 | ||
|
|
7d7e95ac55 | ||
|
|
6d7694caff | ||
|
|
d7b6243698 | ||
|
|
73feef9e92 | ||
|
|
453a546574 | ||
|
|
52c0e6f1f5 | ||
|
|
444f8d87b3 | ||
|
|
57a586c3a7 | ||
|
|
1975265e6b | ||
|
|
66e6cb8dbc | ||
|
|
9ce9d254f8 | ||
|
|
1beca4bfa6 | ||
|
|
82ab29cfc5 | ||
|
|
3579c66f71 | ||
|
|
c042a8e310 | ||
|
|
8d2794a4ee | ||
|
|
50be1d9345 | ||
|
|
c551bf03b6 | ||
|
|
cd062293fc | ||
|
|
e89ea47d3a | ||
|
|
2cd209a6a4 | ||
|
|
9bbc761736 | ||
|
|
9097faa04b | ||
|
|
fcf844cf1a | ||
|
|
8808c31e98 | ||
|
|
e0a9f5a08a | ||
|
|
56d71c8e54 | ||
|
|
125ab4c671 | ||
|
|
8014216c45 | ||
|
|
55ba331489 | ||
|
|
ad2ff672b0 | ||
|
|
00907ecd17 | ||
|
|
07d8219136 | ||
|
|
f37241c84c | ||
|
|
65d046132d | ||
|
|
122cf40092 | ||
|
|
28ed5c86c7 | ||
|
|
1f99c3d895 | ||
|
|
f2293713de | ||
|
|
b3f202400c | ||
|
|
010d87bd0d | ||
|
|
b403b8f09e | ||
|
|
b9a3dc795b | ||
|
|
35dbfdebcf | ||
|
|
c5e5fb3ace | ||
|
|
e649472b20 | ||
|
|
3cbb24a4c5 | ||
|
|
f92608a9d3 | ||
|
|
6591cdc5c1 | ||
|
|
0ae1ac367d | ||
|
|
6d3a1b93e1 | ||
|
|
6d7b22a21c | ||
|
|
784ee22623 | ||
|
|
c03654ef8e | ||
|
|
826cb3117d | ||
|
|
f77fa26ffe | ||
|
|
35e30f9184 | ||
|
|
7dd3ade678 | ||
|
|
6d1e15d11a | ||
|
|
f5b33922ff | ||
|
|
ceb7baf851 | ||
|
|
d195fd3244 | ||
|
|
231cd632d6 | ||
|
|
82d72ea39c | ||
|
|
022bebb14f | ||
|
|
0981ae1b4a | ||
|
|
9608824a28 | ||
|
|
33d215533e | ||
|
|
5c503ecac0 | ||
|
|
d114693fed | ||
|
|
7a8cb80413 | ||
|
|
f5cd234c4b | ||
|
|
49bed5e6a6 | ||
|
|
b84a51235d | ||
|
|
602d6a2337 | ||
|
|
6e614cd3f2 | ||
|
|
6793edd68b | ||
|
|
ad6e3267c3 | ||
|
|
f941117ca4 | ||
|
|
aef0bf03e3 | ||
|
|
f22f6b74db | ||
|
|
913c4ae24e | ||
|
|
4b7b5fa21a | ||
|
|
bf6887541b | ||
|
|
26da9f3a37 | ||
|
|
d48520efdf | ||
|
|
d462356122 | ||
|
|
9a5cdb0a99 | ||
|
|
eaf012d5ff | ||
|
|
19934dad72 | ||
|
|
6194f73442 | ||
|
|
dbc880fe35 | ||
|
|
be4e46a3c6 | ||
|
|
2fce89a689 | ||
|
|
81d21b0907 | ||
|
|
65381b1dc5 | ||
|
|
7cbede2f6e | ||
|
|
0a13dddaea | ||
|
|
662be980e8 | ||
|
|
209abf466d | ||
|
|
db9a3bd562 | ||
|
|
36ecaa6610 | ||
|
|
4f46d0f4a3 | ||
|
|
42ad47649d | ||
|
|
c62ee6e692 | ||
|
|
b38c8d7d5f | ||
|
|
83bcc39d5f | ||
|
|
8d317d1e2c | ||
|
|
9acad2e83c | ||
|
|
9099c5a92c | ||
|
|
60c4d60d66 | ||
|
|
e8a4cde643 | ||
|
|
148eab31b6 |
7
.github/copilot-instructions.md
vendored
7
.github/copilot-instructions.md
vendored
@@ -22,11 +22,13 @@ You are an assistant helping with development of the Home Assistant frontend. Th
|
||||
```bash
|
||||
yarn lint # ESLint + Prettier + TypeScript + Lit
|
||||
yarn format # Auto-fix ESLint + Prettier
|
||||
yarn lint:types # TypeScript compiler
|
||||
yarn lint:types # TypeScript compiler (run WITHOUT file arguments)
|
||||
yarn test # Vitest
|
||||
script/develop # Development server
|
||||
```
|
||||
|
||||
> **WARNING:** Never run `tsc` or `yarn lint:types` with file arguments (e.g., `yarn lint:types src/file.ts`). When `tsc` receives file arguments, it ignores `tsconfig.json` and emits `.js` files into `src/`, polluting the codebase. Always run `yarn lint:types` without arguments. For individual file type checking, rely on IDE diagnostics. If `.js` files are accidentally generated, clean up with `git clean -fd src/`.
|
||||
|
||||
### Component Prefixes
|
||||
|
||||
- `ha-` - Home Assistant components
|
||||
@@ -619,7 +621,6 @@ this.hass.localize("ui.panel.config.updates.update_available", {
|
||||
|
||||
#### Key Terminology
|
||||
|
||||
- **"add-on"** (hyphenated, not "addon")
|
||||
- **"integration"** (preferred over "component")
|
||||
- **Technical terms**: Use lowercase (automation, entity, device, service)
|
||||
|
||||
@@ -711,7 +712,7 @@ this.hass.localize("ui.panel.config.automation.delete_confirm", {
|
||||
- [ ] American English spelling
|
||||
- [ ] Friendly, informational tone
|
||||
- [ ] Avoids abbreviations and jargon
|
||||
- [ ] Correct terminology (add-on not addon, integration not component)
|
||||
- [ ] Correct terminology (integration not component)
|
||||
|
||||
### Component-Specific Checks
|
||||
|
||||
|
||||
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
17
.github/workflows/ci.yaml
vendored
17
.github/workflows/ci.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -94,6 +94,13 @@ jobs:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
- name: Upload frontend build
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: frontend-build
|
||||
path: hass_frontend/
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
supervisor:
|
||||
name: Build supervisor
|
||||
needs: [lint, test]
|
||||
@@ -102,7 +109,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
|
||||
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@c45aaa919ef85620af54242a241ac17a8fa35983 # v3.2.1
|
||||
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
||||
19
.github/workflows/release.yaml
vendored
19
.github/workflows/release.yaml
vendored
@@ -19,8 +19,11 @@ jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
id-token: write # For "Trusted Publisher" to PyPi
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
@@ -34,7 +37,7 @@ jobs:
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -46,14 +49,18 @@ jobs:
|
||||
run: ./script/translations_download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
|
||||
- name: Build and release package
|
||||
run: |
|
||||
python3 -m pip install twine build
|
||||
export TWINE_USERNAME="__token__"
|
||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||
python3 -m pip install build
|
||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
||||
script/release
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
@@ -93,7 +100,7 @@ jobs:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -122,7 +129,7 @@ jobs:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,7 +15,7 @@ dist/
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
/node_modules/
|
||||
node_modules/
|
||||
yarn-error.log
|
||||
npm-debug.log
|
||||
|
||||
|
||||
@@ -205,6 +205,7 @@ const createRspackConfig = ({
|
||||
"lit/decorators$": "lit/decorators.js",
|
||||
"lit/directive$": "lit/directive.js",
|
||||
"lit/directives/until$": "lit/directives/until.js",
|
||||
"lit/directives/ref$": "lit/directives/ref.js",
|
||||
"lit/directives/class-map$": "lit/directives/class-map.js",
|
||||
"lit/directives/style-map$": "lit/directives/style-map.js",
|
||||
"lit/directives/if-defined$": "lit/directives/if-defined.js",
|
||||
@@ -213,7 +214,9 @@ const createRspackConfig = ({
|
||||
"lit/directives/join$": "lit/directives/join.js",
|
||||
"lit/directives/repeat$": "lit/directives/repeat.js",
|
||||
"lit/directives/live$": "lit/directives/live.js",
|
||||
"lit/directives/keyed$": "lit/directives/keyed.js",
|
||||
"lit/directives/keyed$": latestBuild
|
||||
? "lit/directives/keyed.js"
|
||||
: path.resolve(__dirname, "../src/common/lit/keyed-es5.ts"),
|
||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||
"@lit-labs/virtualizer/layouts/grid":
|
||||
"@lit-labs/virtualizer/layouts/grid.js",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockAreaRegistry = (
|
||||
|
||||
@@ -187,5 +187,11 @@ export default tseslint.config(
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/util/recorder-worklet.js"],
|
||||
languageOptions: {
|
||||
globals: globals.audioWorklet,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
|
||||
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
@@ -169,7 +169,7 @@ const SCHEMAS: {
|
||||
{
|
||||
title: "Selectors",
|
||||
translations: {
|
||||
addon: "Addon",
|
||||
addon: "App",
|
||||
entity: "Entity",
|
||||
device: "Device",
|
||||
area: "Area",
|
||||
|
||||
@@ -11,7 +11,7 @@ import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
@@ -239,7 +239,7 @@ const SCHEMAS: {
|
||||
selector: { config_entry: {} },
|
||||
},
|
||||
duration: { name: "Duration", selector: { duration: {} } },
|
||||
addon: { name: "Addon", selector: { addon: {} } },
|
||||
addon: { name: "App", selector: { addon: {} } },
|
||||
number_box: {
|
||||
name: "Number Box",
|
||||
selector: {
|
||||
|
||||
@@ -83,10 +83,10 @@ export class HassioAddonRepositoryEl extends LitElement {
|
||||
? this.supervisor.localize(
|
||||
"common.new_version_available"
|
||||
)
|
||||
: this.supervisor.localize("addon.state.installed")
|
||||
: this.supervisor.localize("app.state.installed")
|
||||
: addon.available
|
||||
? this.supervisor.localize("addon.state.not_installed")
|
||||
: this.supervisor.localize("addon.state.not_available")}
|
||||
? this.supervisor.localize("app.state.not_installed")
|
||||
: this.supervisor.localize("app.state.not_available")}
|
||||
.iconClass=${addon.installed
|
||||
? addon.update_available
|
||||
? "update"
|
||||
|
||||
@@ -120,7 +120,7 @@ export class HassioAddonStore extends LitElement {
|
||||
? html`
|
||||
<div class="advanced">
|
||||
<a href="/profile" target="_top">
|
||||
${this.supervisor.localize("store.missing_addons")}
|
||||
${this.supervisor.localize("store.missing_apps")}
|
||||
</a>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -44,7 +44,7 @@ class HassioAddonAudio extends LitElement {
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.supervisor.localize("addon.configuration.audio.header")}
|
||||
.header=${this.supervisor.localize("app.configuration.audio.header")}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
@@ -52,9 +52,7 @@ class HassioAddonAudio extends LitElement {
|
||||
: nothing}
|
||||
${this._inputDevices &&
|
||||
html`<ha-select
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.audio.input"
|
||||
)}
|
||||
.label=${this.supervisor.localize("app.configuration.audio.input")}
|
||||
@selected=${this._setInputDevice}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
@@ -72,9 +70,7 @@ class HassioAddonAudio extends LitElement {
|
||||
</ha-select>`}
|
||||
${this._outputDevices &&
|
||||
html`<ha-select
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.audio.output"
|
||||
)}
|
||||
.label=${this.supervisor.localize("app.configuration.audio.output")}
|
||||
@selected=${this._setOutputDevice}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
@@ -153,7 +149,7 @@ class HassioAddonAudio extends LitElement {
|
||||
|
||||
const noDevice: HassioHardwareAudioDevice = {
|
||||
device: "default",
|
||||
name: this.supervisor.localize("addon.configuration.audio.default"),
|
||||
name: this.supervisor.localize("app.configuration.audio.default"),
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -81,7 +81,7 @@ class HassioAddonConfigDashboard extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: this.supervisor.localize("addon.configuration.no_configuration")}
|
||||
: this.supervisor.localize("app.configuration.no_configuration")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ class HassioAddonConfig extends LitElement {
|
||||
<ha-card outlined>
|
||||
<div class="header">
|
||||
<h2>
|
||||
${this.supervisor.localize("addon.configuration.options.header")}
|
||||
${this.supervisor.localize("app.configuration.options.header")}
|
||||
</h2>
|
||||
<div class="card-menu">
|
||||
<ha-button-menu @action=${this._handleAction}>
|
||||
@@ -231,10 +231,10 @@ class HassioAddonConfig extends LitElement {
|
||||
<ha-list-item .disabled=${!this._canShowSchema || this.disabled}>
|
||||
${this._yamlMode
|
||||
? this.supervisor.localize(
|
||||
"addon.configuration.options.edit_in_ui"
|
||||
"app.configuration.options.edit_in_ui"
|
||||
)
|
||||
: this.supervisor.localize(
|
||||
"addon.configuration.options.edit_in_yaml"
|
||||
"app.configuration.options.edit_in_yaml"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
@@ -279,7 +279,7 @@ class HassioAddonConfig extends LitElement {
|
||||
: html`
|
||||
<ha-alert alert-type="error">
|
||||
${this.supervisor.localize(
|
||||
"addon.configuration.options.invalid_yaml"
|
||||
"app.configuration.options.invalid_yaml"
|
||||
)}
|
||||
</ha-alert>
|
||||
`}
|
||||
@@ -288,7 +288,7 @@ class HassioAddonConfig extends LitElement {
|
||||
? html`<ha-formfield
|
||||
class="show-additional"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.options.show_unused_optional"
|
||||
"app.configuration.options.show_unused_optional"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
@@ -397,7 +397,7 @@ class HassioAddonConfig extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_reset", {
|
||||
this._error = this.supervisor.localize("app.failed_to_reset", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -440,7 +440,7 @@ class HassioAddonConfig extends LitElement {
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
eventdata.success = false;
|
||||
|
||||
@@ -56,14 +56,12 @@ class HassioAddonNetwork extends LitElement {
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.supervisor.localize(
|
||||
"addon.configuration.network.header"
|
||||
)}
|
||||
.header=${this.supervisor.localize("app.configuration.network.header")}
|
||||
>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.supervisor.localize(
|
||||
"addon.configuration.network.introduction"
|
||||
"app.configuration.network.introduction"
|
||||
)}
|
||||
</p>
|
||||
${this._error
|
||||
@@ -87,7 +85,7 @@ class HassioAddonNetwork extends LitElement {
|
||||
? html`<ha-formfield
|
||||
class="show-optional"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.network.show_disabled"
|
||||
"app.configuration.network.show_disabled"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
@@ -187,7 +185,7 @@ class HassioAddonNetwork extends LitElement {
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_reset", {
|
||||
this._error = this.supervisor.localize("app.failed_to_reset", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
button.actionError();
|
||||
@@ -229,7 +227,7 @@ class HassioAddonNetwork extends LitElement {
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
button.actionError();
|
||||
|
||||
@@ -83,7 +83,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.documentation.get_documentation",
|
||||
"app.documentation.get_documentation",
|
||||
{ error: extractApiErrorMessage(err) }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
mdiCogs,
|
||||
mdiFileDocument,
|
||||
mdiInformationVariant,
|
||||
mdiMathLog,
|
||||
mdiTextBoxOutline,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
@@ -89,7 +89,7 @@ class HassioAddonDashboard extends LitElement {
|
||||
|
||||
const addonTabs: PageNavigation[] = [
|
||||
{
|
||||
translationKey: "addon.panel.info",
|
||||
translationKey: "app.panel.info",
|
||||
path: `/hassio/addon/${this.addon.slug}/info`,
|
||||
iconPath: mdiInformationVariant,
|
||||
},
|
||||
@@ -97,7 +97,7 @@ class HassioAddonDashboard extends LitElement {
|
||||
|
||||
if (this.addon.documentation) {
|
||||
addonTabs.push({
|
||||
translationKey: "addon.panel.documentation",
|
||||
translationKey: "app.panel.documentation",
|
||||
path: `/hassio/addon/${this.addon.slug}/documentation`,
|
||||
iconPath: mdiFileDocument,
|
||||
});
|
||||
@@ -106,14 +106,14 @@ class HassioAddonDashboard extends LitElement {
|
||||
if (this.addon.version) {
|
||||
addonTabs.push(
|
||||
{
|
||||
translationKey: "addon.panel.configuration",
|
||||
translationKey: "app.panel.configuration",
|
||||
path: `/hassio/addon/${this.addon.slug}/config`,
|
||||
iconPath: mdiCogs,
|
||||
},
|
||||
{
|
||||
translationKey: "addon.panel.log",
|
||||
translationKey: "app.panel.log",
|
||||
path: `/hassio/addon/${this.addon.slug}/logs`,
|
||||
iconPath: mdiMathLog,
|
||||
iconPath: mdiTextBoxOutline,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -195,10 +195,10 @@ class HassioAddonDashboard extends LitElement {
|
||||
) {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("my.add_addon_repository_title"),
|
||||
title: this.supervisor.localize("my.add_app_repository_title"),
|
||||
text: this.supervisor.localize(
|
||||
"my.add_addon_repository_description",
|
||||
{ addon: requestedAddon, repository: requestedAddonRepository }
|
||||
"my.add_app_repository_description",
|
||||
{ app: requestedAddon, repository: requestedAddonRepository }
|
||||
),
|
||||
confirmText: this.supervisor.localize("common.add"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
@@ -224,7 +224,7 @@ class HassioAddonDashboard extends LitElement {
|
||||
(addon) => addon.slug === requestedAddon
|
||||
);
|
||||
if (!validAddon) {
|
||||
this._error = this.supervisor.localize("my.error_addon_not_found");
|
||||
this._error = this.supervisor.localize("my.error_app_not_found");
|
||||
} else {
|
||||
navigate(`/hassio/addon/${requestedAddon}`, { replace: true });
|
||||
}
|
||||
|
||||
@@ -150,11 +150,11 @@ class HassioAddonInfo extends LitElement {
|
||||
: undefined;
|
||||
const metrics = [
|
||||
{
|
||||
description: this.supervisor.localize("addon.dashboard.cpu_usage"),
|
||||
description: this.supervisor.localize("app.dashboard.cpu_usage"),
|
||||
value: this._metrics?.cpu_percent,
|
||||
},
|
||||
{
|
||||
description: this.supervisor.localize("addon.dashboard.ram_usage"),
|
||||
description: this.supervisor.localize("app.dashboard.ram_usage"),
|
||||
value: this._metrics?.memory_percent,
|
||||
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
|
||||
this._metrics?.memory_limit
|
||||
@@ -181,11 +181,11 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.supervisor.localize(
|
||||
"addon.dashboard.protection_mode.title"
|
||||
"app.dashboard.protection_mode.title"
|
||||
)}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.protection_mode.content"
|
||||
"app.dashboard.protection_mode.content"
|
||||
)}
|
||||
<ha-button
|
||||
variant="danger"
|
||||
@@ -193,7 +193,7 @@ class HassioAddonInfo extends LitElement {
|
||||
@click=${this._protectionToggled}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.protection_mode.enable"
|
||||
"app.dashboard.protection_mode.enable"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-alert>
|
||||
@@ -220,7 +220,7 @@ class HassioAddonInfo extends LitElement {
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.title=${this.supervisor.localize(
|
||||
"dashboard.addon_running"
|
||||
"dashboard.app_running"
|
||||
)}
|
||||
class="running"
|
||||
.path=${mdiPlayCircle}
|
||||
@@ -229,7 +229,7 @@ class HassioAddonInfo extends LitElement {
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
.title=${this.supervisor.localize(
|
||||
"dashboard.addon_stopped"
|
||||
"dashboard.app_stopped"
|
||||
)}
|
||||
class="stopped"
|
||||
.path=${mdiCircleOffOutline}
|
||||
@@ -242,22 +242,19 @@ class HassioAddonInfo extends LitElement {
|
||||
<div class="description light-color">
|
||||
${this.addon.version
|
||||
? html`
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.current_version",
|
||||
{ version: this.addon.version }
|
||||
)}
|
||||
${this.supervisor.localize("app.dashboard.current_version", {
|
||||
version: this.addon.version,
|
||||
})}
|
||||
<div class="changelog" @click=${this._openChangelog}>
|
||||
(<span class="changelog-link"
|
||||
>${this.supervisor.localize(
|
||||
"addon.dashboard.changelog"
|
||||
"app.dashboard.changelog"
|
||||
)}</span
|
||||
>)
|
||||
</div>
|
||||
`
|
||||
: html`<span class="changelog-link" @click=${this._openChangelog}
|
||||
>${this.supervisor.localize(
|
||||
"addon.dashboard.changelog"
|
||||
)}</span
|
||||
>${this.supervisor.localize("app.dashboard.changelog")}</span
|
||||
>`}
|
||||
</div>
|
||||
|
||||
@@ -274,7 +271,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="stage"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
`addon.dashboard.capability.stages.${this.addon.stage}`
|
||||
`app.dashboard.capability.stages.${this.addon.stage}`
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -298,7 +295,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="rating"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.rating"
|
||||
"app.dashboard.capability.label.rating"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -313,7 +310,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="host_network"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.host"
|
||||
"app.dashboard.capability.label.host"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -329,7 +326,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="full_access"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.hardware"
|
||||
"app.dashboard.capability.label.hardware"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -345,7 +342,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="homeassistant_api"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.core"
|
||||
"app.dashboard.capability.label.core"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -364,7 +361,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="hassio_api"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
`addon.dashboard.capability.role.${this.addon.hassio_role}`
|
||||
`app.dashboard.capability.role.${this.addon.hassio_role}`
|
||||
) || this.addon.hassio_role
|
||||
)}
|
||||
>
|
||||
@@ -383,7 +380,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="docker_api"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.docker"
|
||||
"app.dashboard.capability.label.docker"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -399,7 +396,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="host_pid"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.host_pid"
|
||||
"app.dashboard.capability.label.host_pid"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -416,7 +413,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="apparmor"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.apparmor"
|
||||
"app.dashboard.capability.label.apparmor"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -432,7 +429,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="auth_api"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.auth"
|
||||
"app.dashboard.capability.label.auth"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -448,7 +445,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="ingress"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.ingress"
|
||||
"app.dashboard.capability.label.ingress"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -467,7 +464,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="signed"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.signed"
|
||||
"app.dashboard.capability.label.signed"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -482,7 +479,7 @@ class HassioAddonInfo extends LitElement {
|
||||
@click=${this._showSystemManagedDialog}
|
||||
id="system_managed"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize("addon.system_managed.badge")
|
||||
this.supervisor.localize("app.system_managed.badge")
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@@ -496,7 +493,7 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
<div class="description light-color">
|
||||
${this.addon.description}.<br />
|
||||
${this.supervisor.localize("addon.dashboard.visit_addon_page", {
|
||||
${this.supervisor.localize("app.dashboard.visit_app_page", {
|
||||
name: html`<a
|
||||
href=${this.addon.url!}
|
||||
target="_blank"
|
||||
@@ -527,12 +524,12 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.boot.title"
|
||||
"app.dashboard.option.boot.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.boot.description"
|
||||
"app.dashboard.option.boot.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -548,12 +545,12 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.watchdog.title"
|
||||
"app.dashboard.option.watchdog.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.watchdog.description"
|
||||
"app.dashboard.option.watchdog.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -572,12 +569,12 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.auto_update.title"
|
||||
"app.dashboard.option.auto_update.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.auto_update.description"
|
||||
"app.dashboard.option.auto_update.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -595,12 +592,12 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.ingress_panel.title"
|
||||
"app.dashboard.option.ingress_panel.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.ingress_panel.description"
|
||||
"app.dashboard.option.ingress_panel.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -618,12 +615,12 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.protected.title"
|
||||
"app.dashboard.option.protected.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.protected.description"
|
||||
"app.dashboard.option.protected.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -644,7 +641,7 @@ class HassioAddonInfo extends LitElement {
|
||||
${this.addon.version && this.addon.state === "started"
|
||||
? html`<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("addon.dashboard.hostname")}
|
||||
${this.supervisor.localize("app.dashboard.hostname")}
|
||||
</span>
|
||||
<code slot="description"> ${this.addon.hostname} </code>
|
||||
</ha-settings-row>
|
||||
@@ -671,14 +668,14 @@ class HassioAddonInfo extends LitElement {
|
||||
? html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.not_available_arch"
|
||||
"app.dashboard.not_available_arch"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.not_available_version",
|
||||
"app.dashboard.not_available_version",
|
||||
{
|
||||
core_version_installed: this.supervisor.core.version,
|
||||
core_version_needed: addonStoreInfo!.homeassistant,
|
||||
@@ -699,14 +696,14 @@ class HassioAddonInfo extends LitElement {
|
||||
@click=${this._stopClicked}
|
||||
.disabled=${systemManaged && !this.controlEnabled}
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.stop")}
|
||||
${this.supervisor.localize("app.dashboard.stop")}
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
@click=${this._restartClicked}
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.restart")}
|
||||
${this.supervisor.localize("app.dashboard.restart")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: html`
|
||||
@@ -715,7 +712,7 @@ class HassioAddonInfo extends LitElement {
|
||||
.progress=${this.addon.state === "startup"}
|
||||
appearance="plain"
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.start")}
|
||||
${this.supervisor.localize("app.dashboard.start")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: nothing}
|
||||
@@ -729,7 +726,7 @@ class HassioAddonInfo extends LitElement {
|
||||
@click=${this._uninstallClicked}
|
||||
.disabled=${systemManaged && !this.controlEnabled}
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.uninstall")}
|
||||
${this.supervisor.localize("app.dashboard.uninstall")}
|
||||
</ha-progress-button>
|
||||
${this.addon.build
|
||||
? html`
|
||||
@@ -738,7 +735,7 @@ class HassioAddonInfo extends LitElement {
|
||||
appearance="plain"
|
||||
@click=${this._rebuildClicked}
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.rebuild")}
|
||||
${this.supervisor.localize("app.dashboard.rebuild")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: nothing}
|
||||
@@ -761,7 +758,7 @@ class HassioAddonInfo extends LitElement {
|
||||
: undefined}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.open_web_ui"
|
||||
"app.dashboard.open_web_ui"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
@@ -772,7 +769,7 @@ class HassioAddonInfo extends LitElement {
|
||||
.disabled=${!this.addon.available}
|
||||
@click=${this._installClicked}
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.install")}
|
||||
${this.supervisor.localize("app.dashboard.install")}
|
||||
</ha-progress-button>
|
||||
`}
|
||||
</div>
|
||||
@@ -804,7 +801,7 @@ class HassioAddonInfo extends LitElement {
|
||||
"state" in this.addon &&
|
||||
this.addon.state === "startup"
|
||||
) {
|
||||
// Addon is starting up, wait for it to start
|
||||
// App is starting up, wait for it to start
|
||||
this._scheduleDataUpdate();
|
||||
}
|
||||
}
|
||||
@@ -858,11 +855,11 @@ class HassioAddonInfo extends LitElement {
|
||||
private _showMoreInfo(ev): void {
|
||||
const id = ev.currentTarget.id as AddonCapability;
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: this.supervisor.localize(`addon.dashboard.capability.${id}.title`),
|
||||
title: this.supervisor.localize(`app.dashboard.capability.${id}.title`),
|
||||
content:
|
||||
id === "stage"
|
||||
? this.supervisor.localize(
|
||||
`addon.dashboard.capability.${id}.description`,
|
||||
`app.dashboard.capability.${id}.description`,
|
||||
{
|
||||
icon_stable: `<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon>`,
|
||||
icon_experimental: `<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon>`,
|
||||
@@ -870,7 +867,7 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
)
|
||||
: this.supervisor.localize(
|
||||
`addon.dashboard.capability.${id}.description`
|
||||
`app.dashboard.capability.${id}.description`
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -902,7 +899,7 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
|
||||
private _openIngress(): void {
|
||||
navigate(`/hassio/ingress/${this.addon.slug}`);
|
||||
navigate(`/app/${this.addon.slug}`);
|
||||
}
|
||||
|
||||
private get _computeShowIngressUI(): boolean {
|
||||
@@ -936,7 +933,7 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -956,7 +953,7 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -976,7 +973,7 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -996,7 +993,7 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1016,7 +1013,7 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1030,13 +1027,13 @@ class HassioAddonInfo extends LitElement {
|
||||
);
|
||||
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.changelog"),
|
||||
title: this.supervisor.localize("app.dashboard.changelog"),
|
||||
content: extractChangelog(this.addon as HassioAddonDetails, content),
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"addon.dashboard.action_error.get_changelog"
|
||||
"app.dashboard.action_error.get_changelog"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
@@ -1066,7 +1063,7 @@ class HassioAddonInfo extends LitElement {
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.install"),
|
||||
title: this.supervisor.localize("app.dashboard.action_error.install"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1091,7 +1088,7 @@ class HassioAddonInfo extends LitElement {
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.stop"),
|
||||
title: this.supervisor.localize("app.dashboard.action_error.stop"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1112,7 +1109,7 @@ class HassioAddonInfo extends LitElement {
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.restart"),
|
||||
title: this.supervisor.localize("app.dashboard.action_error.restart"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1127,7 +1124,7 @@ class HassioAddonInfo extends LitElement {
|
||||
await rebuildLocalAddon(this.hass, this.addon.slug);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.rebuild"),
|
||||
title: this.supervisor.localize("app.dashboard.action_error.rebuild"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1145,12 +1142,12 @@ class HassioAddonInfo extends LitElement {
|
||||
if (!validate.valid) {
|
||||
await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"addon.dashboard.action_error.start_invalid_config"
|
||||
"app.dashboard.action_error.start_invalid_config"
|
||||
),
|
||||
text: validate.message.split(" Got ")[0],
|
||||
confirm: () => this._openConfiguration(),
|
||||
confirmText: this.supervisor.localize(
|
||||
"addon.dashboard.action_error.go_to_config"
|
||||
"app.dashboard.action_error.go_to_config"
|
||||
),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
@@ -1162,7 +1159,7 @@ class HassioAddonInfo extends LitElement {
|
||||
button.actionError();
|
||||
button.progress = false;
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to validate addon configuration",
|
||||
title: "Failed to validate app configuration",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
@@ -1181,7 +1178,7 @@ class HassioAddonInfo extends LitElement {
|
||||
button.actionError();
|
||||
button.progress = false;
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.start"),
|
||||
title: this.supervisor.localize("app.dashboard.action_error.start"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
@@ -1207,13 +1204,13 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("dialog.uninstall_addon.title", {
|
||||
title: this.supervisor.localize("dialog.uninstall_app.title", {
|
||||
name: this.addon.name,
|
||||
}),
|
||||
text: html`
|
||||
<ha-formfield
|
||||
.label=${html`<p>
|
||||
${this.supervisor.localize("dialog.uninstall_addon.remove_data")}
|
||||
${this.supervisor.localize("dialog.uninstall_app.remove_data")}
|
||||
</p>`}
|
||||
>
|
||||
<ha-switch
|
||||
@@ -1223,7 +1220,7 @@ class HassioAddonInfo extends LitElement {
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
`,
|
||||
confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"),
|
||||
confirmText: this.supervisor.localize("dialog.uninstall_app.uninstall"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
@@ -1245,9 +1242,7 @@ class HassioAddonInfo extends LitElement {
|
||||
button.actionSuccess();
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"addon.dashboard.action_error.uninstall"
|
||||
),
|
||||
title: this.supervisor.localize("app.dashboard.action_error.uninstall"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
button.actionError();
|
||||
|
||||
@@ -19,14 +19,14 @@ class HassioAddonSystemManaged extends LitElement {
|
||||
return html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.supervisor.localize("addon.system_managed.title")}
|
||||
.title=${this.supervisor.localize("app.system_managed.title")}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
${this.supervisor.localize("addon.system_managed.description")}
|
||||
${this.supervisor.localize("app.system_managed.description")}
|
||||
${!this.hideButton
|
||||
? html`
|
||||
<ha-button slot="action" @click=${this._takeControl}>
|
||||
${this.supervisor.localize("addon.system_managed.take_control")}
|
||||
${this.supervisor.localize("app.system_managed.take_control")}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
@@ -216,7 +216,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this.supervisor?.localize("backup.addons")}
|
||||
.label=${this.supervisor?.localize("backup.apps")}
|
||||
.iconPath=${mdiPuzzle}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
|
||||
@@ -33,13 +33,13 @@ class HassioAddons extends LitElement {
|
||||
suffix
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.supervisor.localize("dashboard.search_addons")}
|
||||
.label=${this.supervisor.localize("dashboard.search_apps")}
|
||||
>
|
||||
</search-input>
|
||||
</div>
|
||||
<div class="content">
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? html`<h1>${this.supervisor.localize("dashboard.addons")}</h1>`
|
||||
? html`<h1>${this.supervisor.localize("dashboard.apps")}</h1>`
|
||||
: ""}
|
||||
<div class="card-group">
|
||||
${!this.supervisor.addon.addons.length
|
||||
@@ -47,7 +47,7 @@ class HassioAddons extends LitElement {
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<button class="link" @click=${this._openStore}>
|
||||
${this.supervisor.localize("dashboard.no_addons")}
|
||||
${this.supervisor.localize("dashboard.no_apps")}
|
||||
</button>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -67,14 +67,12 @@ class HassioAddons extends LitElement {
|
||||
? mdiArrowUpBoldCircle
|
||||
: mdiPuzzle}
|
||||
.iconTitle=${addon.state !== "started"
|
||||
? this.supervisor.localize("dashboard.addon_stopped")
|
||||
? this.supervisor.localize("dashboard.app_stopped")
|
||||
: addon.update_available!
|
||||
? this.supervisor.localize(
|
||||
"dashboard.addon_new_version"
|
||||
"dashboard.app_new_version"
|
||||
)
|
||||
: this.supervisor.localize(
|
||||
"dashboard.addon_running"
|
||||
)}
|
||||
: this.supervisor.localize("dashboard.app_running")}
|
||||
.iconClass=${addon.update_available
|
||||
? addon.state === "started"
|
||||
? "update"
|
||||
|
||||
@@ -39,7 +39,7 @@ class HassioDashboard extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
.header=${this.supervisor.localize("panel.addons")}
|
||||
.header=${this.supervisor.localize("panel.apps")}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
@@ -81,7 +81,7 @@ class HassioDashboard extends LitElement {
|
||||
<span slot="header">
|
||||
${this.supervisor.localize(
|
||||
atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
? "panel.apps"
|
||||
: "panel.dashboard"
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -64,9 +64,9 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
repos
|
||||
.filter(
|
||||
(repo) =>
|
||||
repo.slug !== "core" && // The core add-ons repository
|
||||
repo.slug !== "local" && // Locally managed add-ons
|
||||
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
|
||||
repo.slug !== "core" && // The core apps repository
|
||||
repo.slug !== "local" && // Locally managed apps
|
||||
repo.slug !== "a0d7b954" && // Home Assistant Community Apps
|
||||
repo.slug !== "5c53de3b" && // The ESPHome repository
|
||||
repo.slug !== "d5369777" // Music Assistant repository
|
||||
)
|
||||
|
||||
@@ -16,11 +16,11 @@ export const suggestAddonRestart = async (
|
||||
addon: HassioAddonDetails
|
||||
): Promise<void> => {
|
||||
const confirmed = await showConfirmationDialog(element, {
|
||||
title: supervisor.localize("dialog.restart_addon.title", {
|
||||
title: supervisor.localize("dialog.restart_app.title", {
|
||||
name: addon.name,
|
||||
}),
|
||||
text: supervisor.localize("dialog.restart_addon.text"),
|
||||
confirmText: supervisor.localize("dialog.restart_addon.restart"),
|
||||
text: supervisor.localize("dialog.restart_app.text"),
|
||||
confirmText: supervisor.localize("dialog.restart_app.restart"),
|
||||
dismissText: supervisor.localize("common.cancel"),
|
||||
});
|
||||
if (confirmed) {
|
||||
|
||||
@@ -89,14 +89,12 @@ class HassioSystemManagedDialog extends LitElement {
|
||||
? html`<img src=${addonImage} alt=${this._addon.name} />`
|
||||
: html`<ha-svg-icon .path=${mdiPuzzle}></ha-svg-icon>`}
|
||||
</div>
|
||||
${this._supervisor.localize("addon.system_managed.title")}.<br />
|
||||
${this._supervisor.localize("addon.system_managed.description")}
|
||||
${this._supervisor.localize("app.system_managed.title")}.<br />
|
||||
${this._supervisor.localize("app.system_managed.description")}
|
||||
${this._configEntry
|
||||
? html`
|
||||
<h3>
|
||||
${this._supervisor.localize(
|
||||
"addon.system_managed.managed_by"
|
||||
)}:
|
||||
${this._supervisor.localize("app.system_managed.managed_by")}:
|
||||
</h3>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item
|
||||
|
||||
@@ -14,7 +14,7 @@ export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] =>
|
||||
: [
|
||||
{
|
||||
translationKey: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
? "panel.apps"
|
||||
: "panel.dashboard",
|
||||
path: `/hassio/dashboard`,
|
||||
iconPath: atLeastVersion(hass.config.version, 2021, 12)
|
||||
|
||||
@@ -114,14 +114,14 @@ class HassioIngressView extends LitElement {
|
||||
}
|
||||
if (!addonInfo.version) {
|
||||
await showAlertDialog(this, {
|
||||
text: this.supervisor.localize("my.error_addon_not_installed"),
|
||||
text: this.supervisor.localize("my.error_app_not_installed"),
|
||||
title: addonInfo.name,
|
||||
});
|
||||
await nextRender();
|
||||
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
|
||||
} else if (!addonInfo.ingress) {
|
||||
await showAlertDialog(this, {
|
||||
text: this.supervisor.localize("my.error_addon_no_ingress"),
|
||||
text: this.supervisor.localize("my.error_app_no_ingress"),
|
||||
title: addonInfo.name,
|
||||
});
|
||||
await nextRender();
|
||||
@@ -162,8 +162,8 @@ class HassioIngressView extends LitElement {
|
||||
await this.updateComplete;
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_info") ||
|
||||
"Unable to fetch add-on info to start Ingress",
|
||||
this.supervisor.localize("ingress.error_app_info") ||
|
||||
"Unable to fetch app info to start Ingress",
|
||||
title: "Supervisor",
|
||||
});
|
||||
await nextRender();
|
||||
@@ -175,8 +175,8 @@ class HassioIngressView extends LitElement {
|
||||
await this.updateComplete;
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_not_installed") ||
|
||||
"The add-on is not installed. Please install it first",
|
||||
this.supervisor.localize("ingress.error_app_not_installed") ||
|
||||
"The app is not installed. Please install it first",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
@@ -188,8 +188,8 @@ class HassioIngressView extends LitElement {
|
||||
await this.updateComplete;
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_not_supported") ||
|
||||
"This add-on does not support Ingress",
|
||||
this.supervisor.localize("ingress.error_app_not_supported") ||
|
||||
"This app does not support Ingress",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
@@ -201,18 +201,18 @@ class HassioIngressView extends LitElement {
|
||||
await this.updateComplete;
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_not_running") ||
|
||||
"The add-on is not running. Do you want to start it now?",
|
||||
this.supervisor.localize("ingress.error_app_not_running") ||
|
||||
"The app is not running. Do you want to start it now?",
|
||||
title: addon.name,
|
||||
confirmText:
|
||||
this.supervisor.localize("ingress.start_addon") || "Start add-on",
|
||||
this.supervisor.localize("ingress.start_app") || "Start app",
|
||||
dismissText: this.supervisor.localize("common.no") || "No",
|
||||
});
|
||||
if (confirm) {
|
||||
try {
|
||||
this._loadingMessage =
|
||||
this.supervisor.localize("ingress.addon_starting") ||
|
||||
"The add-on is starting, this can take some time...";
|
||||
this.supervisor.localize("ingress.app_starting") ||
|
||||
"The app is starting, this can take some time...";
|
||||
await startHassioAddon(this.hass, addonSlug);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "addon",
|
||||
@@ -222,8 +222,8 @@ class HassioIngressView extends LitElement {
|
||||
} catch (_err) {
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_starting_addon") ||
|
||||
"Error starting the add-on",
|
||||
this.supervisor.localize("ingress.error_starting_app") ||
|
||||
"Error starting the app",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
@@ -238,10 +238,10 @@ class HassioIngressView extends LitElement {
|
||||
}
|
||||
|
||||
if (addon.state === "startup") {
|
||||
// Addon is starting up, wait for it to start
|
||||
// App is starting up, wait for it to start
|
||||
this._loadingMessage =
|
||||
this.supervisor.localize("ingress.addon_starting") ||
|
||||
"The add-on is starting, this can take some time...";
|
||||
this.supervisor.localize("ingress.app_starting") ||
|
||||
"The app is starting, this can take some time...";
|
||||
|
||||
this._fetchDataTimeout = window.setTimeout(() => {
|
||||
this._fetchData(addonSlug);
|
||||
@@ -301,8 +301,8 @@ class HassioIngressView extends LitElement {
|
||||
await this.updateComplete;
|
||||
showConfirmationDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_not_ready") ||
|
||||
"The add-on seems to not be ready, it might still be starting. Do you want to try again?",
|
||||
this.supervisor.localize("ingress.error_app_not_ready") ||
|
||||
"The app seems to not be ready, it might still be starting. Do you want to try again?",
|
||||
title: this._addon.name,
|
||||
confirmText: this.supervisor.localize("ingress.retry") || "Retry",
|
||||
dismissText: this.supervisor.localize("common.no") || "No",
|
||||
|
||||
@@ -261,16 +261,16 @@ class UpdateAvailableCard extends LitElement {
|
||||
private _computeCreateBackupTexts():
|
||||
| { title: string; description?: string }
|
||||
| undefined {
|
||||
// Addon backup
|
||||
// App backup
|
||||
if (
|
||||
this._updateType === "addon" &&
|
||||
atLeastVersion(this.hass.config.version, 2025, 2, 0)
|
||||
) {
|
||||
const version = this._version;
|
||||
return {
|
||||
title: this.supervisor.localize("update_available.create_backup.addon"),
|
||||
title: this.supervisor.localize("update_available.create_backup.app"),
|
||||
description: this.supervisor.localize(
|
||||
"update_available.create_backup.addon_description",
|
||||
"update_available.create_backup.app_description",
|
||||
{ version: version }
|
||||
),
|
||||
};
|
||||
@@ -363,11 +363,11 @@ class UpdateAvailableCard extends LitElement {
|
||||
)
|
||||
) {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.dashboard.not_available_arch"
|
||||
"app.dashboard.not_available_arch"
|
||||
);
|
||||
} else {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.dashboard.not_available_version",
|
||||
"app.dashboard.not_available_version",
|
||||
{
|
||||
core_version_installed: this.supervisor.core.version,
|
||||
core_version_needed: addonStoreInfo.homeassistant,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
60
package.json
60
package.json
@@ -26,26 +26,26 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.4",
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.3",
|
||||
"@codemirror/view": "6.39.7",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/view": "6.39.11",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.1.0",
|
||||
"@formatjs/intl-displaynames": "7.1.0",
|
||||
"@formatjs/intl-durationformat": "0.9.0",
|
||||
"@formatjs/intl-getcanonicallocales": "3.1.0",
|
||||
"@formatjs/intl-listformat": "8.1.0",
|
||||
"@formatjs/intl-locale": "5.1.0",
|
||||
"@formatjs/intl-numberformat": "9.1.0",
|
||||
"@formatjs/intl-pluralrules": "6.1.0",
|
||||
"@formatjs/intl-relativetimeformat": "12.1.0",
|
||||
"@formatjs/intl-datetimeformat": "7.2.0",
|
||||
"@formatjs/intl-displaynames": "7.2.0",
|
||||
"@formatjs/intl-durationformat": "0.10.0",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.0",
|
||||
"@formatjs/intl-listformat": "8.2.0",
|
||||
"@formatjs/intl-locale": "5.2.0",
|
||||
"@formatjs/intl-numberformat": "9.2.0",
|
||||
"@formatjs/intl-pluralrules": "6.2.0",
|
||||
"@formatjs/intl-relativetimeformat": "12.2.0",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@@ -112,7 +112,7 @@
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.0.7",
|
||||
"intl-messageformat": "11.1.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -133,7 +133,7 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.7",
|
||||
"ua-parser-js": "2.0.8",
|
||||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
@@ -146,18 +146,18 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.5",
|
||||
"@babel/core": "7.28.6",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||
"@babel/plugin-transform-runtime": "7.28.5",
|
||||
"@babel/preset-env": "7.28.5",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.7",
|
||||
"@babel/preset-env": "7.28.6",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.8",
|
||||
"@lokalise/node-api": "15.6.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.4.0",
|
||||
"@rspack/core": "1.6.8",
|
||||
"@rspack/dev-server": "1.1.4",
|
||||
"@rsdoctor/rspack-plugin": "1.5.0",
|
||||
"@rspack/core": "1.7.2",
|
||||
"@rspack/dev-server": "1.1.5",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -176,7 +176,7 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"@vitest/coverage-v8": "4.0.17",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
@@ -199,7 +199,7 @@
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "27.3.0",
|
||||
"jsdom": "27.4.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -207,17 +207,17 @@
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.7.4",
|
||||
"rspack-manifest-plugin": "5.2.0",
|
||||
"prettier": "3.8.0",
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.1",
|
||||
"tar": "7.5.2",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.50.1",
|
||||
"vite-tsconfig-paths": "6.0.3",
|
||||
"vitest": "4.0.16",
|
||||
"typescript-eslint": "8.53.0",
|
||||
"vite-tsconfig-paths": "6.0.4",
|
||||
"vitest": "4.0.17",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
@@ -229,13 +229,13 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"globals": "16.5.0",
|
||||
"globals": "17.0.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.12.0"
|
||||
"node": "24.13.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/bin/sh
|
||||
# Pushes a new version to PyPi.
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
@@ -12,5 +11,4 @@ yarn install
|
||||
script/build_frontend
|
||||
|
||||
rm -rf dist home_assistant_frontend.egg-info
|
||||
python3 -m build
|
||||
python3 -m twine upload dist/*.whl --skip-existing
|
||||
python3 -m build -q
|
||||
|
||||
@@ -38,13 +38,11 @@ export class HaAuthFormString extends HaFormString {
|
||||
}
|
||||
</style>
|
||||
<ha-auth-textfield
|
||||
.type=${
|
||||
!this.isPassword
|
||||
.type=${!this.isPassword
|
||||
? this.stringType
|
||||
: this.unmaskedPassword
|
||||
? "text"
|
||||
: "password"
|
||||
}
|
||||
: "password"}
|
||||
.label=${this.label}
|
||||
.value=${this.data || ""}
|
||||
.helper=${this.helper}
|
||||
@@ -55,18 +53,17 @@ export class HaAuthFormString extends HaFormString {
|
||||
.name=${this.schema.name}
|
||||
.autocomplete=${this.schema.autocomplete}
|
||||
?autofocus=${this.schema.autofocus}
|
||||
.suffix=${
|
||||
this.isPassword
|
||||
? // reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
: this.schema.description?.suffix
|
||||
}
|
||||
.validationMessage=${this.schema.required ? this.localize?.("ui.panel.page-authorize.form.error_required") : undefined}
|
||||
.suffix=${this.isPassword
|
||||
? // reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
: this.schema.description?.suffix}
|
||||
.validationMessage=${this.schema.required
|
||||
? this.localize?.("ui.panel.page-authorize.form.error_required")
|
||||
: undefined}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
></ha-auth-textfield>
|
||||
${this.renderIcon()}
|
||||
</ha-auth-textfield>
|
||||
></ha-auth-textfield>
|
||||
${this.renderIcon()}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
|
||||
export interface AreasFloorHierarchy {
|
||||
|
||||
@@ -79,7 +79,7 @@ export const generateColorPalette = (
|
||||
}
|
||||
|
||||
return steps.map((step) => {
|
||||
const name = `color-${label}-${step}`;
|
||||
const name = `ha-color-${label}-${step}`;
|
||||
|
||||
// Base color at 50%
|
||||
if (step === 50) {
|
||||
|
||||
@@ -93,8 +93,8 @@ export const calcDateRange = (
|
||||
];
|
||||
case "now-12m":
|
||||
return [
|
||||
calcDate(subMonths(today, 12), startOfMonth, hass.locale, hass.config),
|
||||
calcDate(subMonths(today, 1), endOfMonth, hass.locale, hass.config),
|
||||
calcDate(today, subMonths, hass.locale, hass.config, 12),
|
||||
calcDate(today, subMonths, hass.locale, hass.config, 0),
|
||||
];
|
||||
case "now-1h":
|
||||
return [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
|
||||
export const computeAreaName = (area: AreaRegistryEntry): string | undefined =>
|
||||
area.name?.trim();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
|
||||
53
src/common/lit/keyed-es5.ts
Normal file
53
src/common/lit/keyed-es5.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* ES5-compatible implementation of the keyed directive.
|
||||
* Based on lit-html's keyed directive but written to avoid ES5 minification issues.
|
||||
*
|
||||
* This implementation avoids parameter destructuring in the update() method,
|
||||
* which causes Terser with ecma: 5 to generate invalid references like `_k`.
|
||||
*
|
||||
* Used only for ES5 builds (legacy browsers). Modern builds use the original
|
||||
* lit-html keyed directive.
|
||||
*
|
||||
* @see https://github.com/home-assistant/frontend/issues/28732
|
||||
*/
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { directive, Directive } from "lit-html/directive.js";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { setCommittedValue } from "lit-html/directive-helpers.js";
|
||||
// eslint-disable-next-line lit/no-legacy-imports
|
||||
import { nothing } from "lit-html";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import type { Part } from "lit-html/directive.js";
|
||||
|
||||
class KeyedES5 extends Directive {
|
||||
private _key: unknown = nothing;
|
||||
|
||||
render(k: unknown, v: unknown) {
|
||||
this._key = k;
|
||||
return v;
|
||||
}
|
||||
|
||||
update(part: unknown, args: [unknown, unknown]) {
|
||||
const k = args[0];
|
||||
const v = args[1];
|
||||
if (k !== this._key) {
|
||||
// Clear the part before returning a value. The one-arg form of
|
||||
// setCommittedValue sets the value to a sentinel which forces a
|
||||
// commit the next render.
|
||||
setCommittedValue(part as Part);
|
||||
this._key = k;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates a renderable value with a unique key. When the key changes, the
|
||||
* previous DOM is removed and disposed before rendering the next value, even
|
||||
* if the value - such as a template - is the same.
|
||||
*
|
||||
* This is useful for forcing re-renders of stateful components, or working
|
||||
* with code that expects new data to generate new HTML elements, such as some
|
||||
* animation techniques.
|
||||
*/
|
||||
export const keyed = directive(KeyedES5);
|
||||
14
src/common/url/route.ts
Normal file
14
src/common/url/route.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Route } from "../../types";
|
||||
|
||||
export const computeRouteTail = (route: Route) => {
|
||||
const dividerPos = route.path.indexOf("/", 1);
|
||||
return dividerPos === -1
|
||||
? {
|
||||
prefix: route.prefix + route.path,
|
||||
path: "",
|
||||
}
|
||||
: {
|
||||
prefix: route.prefix + route.path.substring(0, dividerPos),
|
||||
path: route.path.substring(dividerPos),
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,16 @@
|
||||
// From https://github.com/epoberezkin/fast-deep-equal
|
||||
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
|
||||
export const deepEqual = (a: any, b: any): boolean => {
|
||||
|
||||
interface DeepEqualOptions {
|
||||
/** Compare Symbol properties in addition to string keys */
|
||||
compareSymbols?: boolean;
|
||||
}
|
||||
|
||||
export const deepEqual = (
|
||||
a: any,
|
||||
b: any,
|
||||
options?: DeepEqualOptions
|
||||
): boolean => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
@@ -18,7 +28,7 @@ export const deepEqual = (a: any, b: any): boolean => {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
if (!deepEqual(a[i], b[i], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +45,7 @@ export const deepEqual = (a: any, b: any): boolean => {
|
||||
}
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!deepEqual(i[1], b.get(i[0]))) {
|
||||
if (!deepEqual(i[1], b.get(i[0]), options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,11 +103,28 @@ export const deepEqual = (a: any, b: any): boolean => {
|
||||
for (i = length; i-- !== 0; ) {
|
||||
const key = keys[i];
|
||||
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
if (!deepEqual(a[key], b[key], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Compare Symbol properties if requested
|
||||
if (options?.compareSymbols) {
|
||||
const symbolsA = Object.getOwnPropertySymbols(a);
|
||||
const symbolsB = Object.getOwnPropertySymbols(b);
|
||||
if (symbolsA.length !== symbolsB.length) {
|
||||
return false;
|
||||
}
|
||||
for (const sym of symbolsA) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, sym)) {
|
||||
return false;
|
||||
}
|
||||
if (!deepEqual(a[sym], b[sym], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,13 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { LitElement, html, css } from "lit";
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
|
||||
import type { CallbackDataParams } from "echarts/types/src/util/types";
|
||||
import type {
|
||||
CallbackDataParams,
|
||||
ECElementEvent,
|
||||
} from "echarts/types/src/util/types";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import SankeyChart from "../../resources/echarts/components/sankey/install";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
@@ -21,6 +25,7 @@ export interface Node {
|
||||
label?: string;
|
||||
color?: string;
|
||||
passThrough?: boolean;
|
||||
entityId?: string;
|
||||
}
|
||||
export interface Link {
|
||||
source: string;
|
||||
@@ -83,6 +88,7 @@ export class HaSankeyChart extends LitElement {
|
||||
.options=${options}
|
||||
height="100%"
|
||||
.extraComponents=${[SankeyChart]}
|
||||
@chart-click=${this._handleChartClick}
|
||||
></ha-chart-base>`;
|
||||
}
|
||||
|
||||
@@ -103,6 +109,22 @@ export class HaSankeyChart extends LitElement {
|
||||
return null;
|
||||
};
|
||||
|
||||
private _handleChartClick = (ev: CustomEvent<ECElementEvent>) => {
|
||||
const detail = ev.detail;
|
||||
// Only handle node clicks (not links)
|
||||
if (detail.dataType !== "node") {
|
||||
return;
|
||||
}
|
||||
const nodeId = (detail.data as Record<string, any>)?.id;
|
||||
if (!nodeId) {
|
||||
return;
|
||||
}
|
||||
const node = this.data.nodes.find((n) => n.id === nodeId);
|
||||
if (node?.entityId) {
|
||||
fireEvent(this, "node-click", { node });
|
||||
}
|
||||
};
|
||||
|
||||
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
|
||||
const filteredNodes = data.nodes.filter((n) => n.value > 0);
|
||||
const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();
|
||||
@@ -294,4 +316,7 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-sankey-chart": HaSankeyChart;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"node-click": { node: Node };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { measureTextWidth } from "../../util/text";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const parsed = parseFloat(value);
|
||||
@@ -184,7 +185,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
|
||||
if (param.seriesName) {
|
||||
return `${param.marker} ${param.seriesName}: ${value}`;
|
||||
return `${param.marker} ${filterXSS(param.seriesName)}: ${value}`;
|
||||
}
|
||||
return `${param.marker} ${value}`;
|
||||
})
|
||||
|
||||
@@ -1364,6 +1364,9 @@ export class HaDataTable extends LitElement {
|
||||
.mdc-data-table__header-cell > * {
|
||||
transition: var(--float-start) 0.2s ease;
|
||||
}
|
||||
.mdc-data-table__header-cell--numeric > span {
|
||||
transition: none;
|
||||
}
|
||||
.mdc-data-table__header-cell ha-svg-icon {
|
||||
top: -3px;
|
||||
position: absolute;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { expose } from "comlink";
|
||||
import Fuse, { type FuseOptionKey } from "fuse.js";
|
||||
import type { FuseOptionKey, IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ipCompare, stringCompare } from "../../common/string/compare";
|
||||
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
||||
import { multiTermSearch } from "../../resources/fuseMultiTerm";
|
||||
import type {
|
||||
ClonedDataTableColumnData,
|
||||
DataTableRowData,
|
||||
@@ -11,46 +11,159 @@ import type {
|
||||
SortingDirection,
|
||||
} from "./ha-data-table";
|
||||
|
||||
const getSearchKeys = memoizeOne(
|
||||
(columns: SortableColumnContainer): FuseOptionKey<DataTableRowData>[] => {
|
||||
const searchKeys = new Set<string>();
|
||||
interface FilterKeyConfig {
|
||||
key: string;
|
||||
filterKey?: string;
|
||||
}
|
||||
|
||||
Object.entries(columns).forEach(([key, column]) => {
|
||||
if (column.filterable) {
|
||||
searchKeys.add(
|
||||
column.filterKey
|
||||
? `${column.valueColumn || key}.${column.filterKey}`
|
||||
: key
|
||||
);
|
||||
}
|
||||
});
|
||||
return Array.from(searchKeys);
|
||||
const getFilterKeys = memoizeOne(
|
||||
(columns: SortableColumnContainer): FilterKeyConfig[] =>
|
||||
Object.entries(columns)
|
||||
.filter(([, column]) => column.filterable)
|
||||
.map(([key, column]) => ({
|
||||
key: column.valueColumn || key,
|
||||
filterKey: column.filterKey,
|
||||
}))
|
||||
);
|
||||
|
||||
const getSearchableValue = (
|
||||
row: DataTableRowData,
|
||||
{ key, filterKey }: FilterKeyConfig
|
||||
): string => {
|
||||
let value = row[key];
|
||||
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
);
|
||||
|
||||
const fuseIndex = memoizeOne(
|
||||
(data: DataTableRowData[], keys: FuseOptionKey<DataTableRowData>[]) =>
|
||||
Fuse.createIndex(keys, data)
|
||||
);
|
||||
if (filterKey && typeof value === "object" && !Array.isArray(value)) {
|
||||
value = value[filterKey];
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const stringValues = value
|
||||
.filter((item) => item != null && typeof item !== "object")
|
||||
.map(String);
|
||||
return stripDiacritics(stringValues.join(" ").toLowerCase());
|
||||
}
|
||||
|
||||
return stripDiacritics(String(value).toLowerCase());
|
||||
};
|
||||
|
||||
/** Filters data using exact substring matching (all terms must match). */
|
||||
const filterDataExact = (
|
||||
data: DataTableRowData[],
|
||||
filterKeys: FilterKeyConfig[],
|
||||
terms: string[]
|
||||
): DataTableRowData[] => {
|
||||
if (terms.length === 1) {
|
||||
const term = terms[0];
|
||||
return data.filter((row) =>
|
||||
filterKeys.some((config) =>
|
||||
getSearchableValue(row, config).includes(term)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return data.filter((row) => {
|
||||
const searchString = filterKeys
|
||||
.map((config) => getSearchableValue(row, config))
|
||||
.join(" ");
|
||||
return terms.every((term) => searchString.includes(term));
|
||||
});
|
||||
};
|
||||
|
||||
const FUZZY_OPTIONS: IFuseOptions<DataTableRowData> = {
|
||||
ignoreDiacritics: true,
|
||||
isCaseSensitive: false,
|
||||
threshold: 0.2, // Stricter than default 0.3
|
||||
minMatchCharLength: 2,
|
||||
ignoreLocation: true,
|
||||
shouldSort: false,
|
||||
};
|
||||
|
||||
interface FuseKeyConfig {
|
||||
name: string | string[];
|
||||
getFn: (row: DataTableRowData) => string;
|
||||
}
|
||||
|
||||
/** Filters data using fuzzy matching with Fuse.js (all terms must match). */
|
||||
const filterDataFuzzy = (
|
||||
data: DataTableRowData[],
|
||||
filterKeys: FilterKeyConfig[],
|
||||
terms: string[]
|
||||
): DataTableRowData[] => {
|
||||
// Build Fuse.js search keys from filter keys
|
||||
const fuseKeys: FuseKeyConfig[] = filterKeys.map((config) => ({
|
||||
name: config.filterKey ? [config.key, config.filterKey] : config.key,
|
||||
getFn: (row: DataTableRowData) => getSearchableValue(row, config),
|
||||
}));
|
||||
|
||||
// Find minimum term length to adjust minMatchCharLength
|
||||
const minTermLength = Math.min(...terms.map((t) => t.length));
|
||||
const minMatchCharLength = Math.min(minTermLength, 2);
|
||||
|
||||
const fuse = new Fuse<DataTableRowData>(data, {
|
||||
...FUZZY_OPTIONS,
|
||||
keys: fuseKeys as FuseOptionKey<DataTableRowData>[],
|
||||
minMatchCharLength,
|
||||
});
|
||||
|
||||
// For single term, simple search
|
||||
if (terms.length === 1) {
|
||||
return fuse.search(terms[0]).map((r) => r.item);
|
||||
}
|
||||
|
||||
// For multiple terms, all must match (AND logic)
|
||||
const expression = {
|
||||
$and: terms.map((term) => ({
|
||||
$or: fuseKeys.map((key) => ({
|
||||
$path: Array.isArray(key.name) ? key.name : [key.name],
|
||||
$val: term,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
return fuse.search(expression).map((r) => r.item);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters data with exact match priority and fuzzy fallback.
|
||||
* - First tries exact substring matching
|
||||
* - If exact matches found, returns only those
|
||||
* - If no exact matches, falls back to fuzzy search with strict scoring
|
||||
*/
|
||||
const filterData = (
|
||||
data: DataTableRowData[],
|
||||
columns: SortableColumnContainer,
|
||||
filter: string
|
||||
) => {
|
||||
filter = stripDiacritics(filter.toLowerCase());
|
||||
): DataTableRowData[] => {
|
||||
const normalizedFilter = stripDiacritics(filter.toLowerCase().trim());
|
||||
|
||||
if (filter === "") {
|
||||
if (!normalizedFilter) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const keys = getSearchKeys(columns);
|
||||
const filterKeys = getFilterKeys(columns);
|
||||
|
||||
const index = fuseIndex(data, keys);
|
||||
if (!filterKeys.length) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return multiTermSearch<DataTableRowData>(data, filter, keys, index, {
|
||||
threshold: 0.2, // reduce fuzzy matches in data tables
|
||||
});
|
||||
const terms = normalizedFilter.split(/\s+/);
|
||||
|
||||
// First, try exact substring matching
|
||||
const exactMatches = filterDataExact(data, filterKeys, terms);
|
||||
|
||||
if (exactMatches.length > 0) {
|
||||
return exactMatches;
|
||||
}
|
||||
|
||||
// No exact matches, fall back to fuzzy search
|
||||
return filterDataFuzzy(data, filterKeys, terms);
|
||||
};
|
||||
|
||||
const sortData = (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import type { DeviceAutomation } from "../../data/device/device_automation";
|
||||
import {
|
||||
@@ -11,11 +12,12 @@ import {
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-md-select";
|
||||
import "../ha-md-select-option";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
|
||||
|
||||
export abstract class HaDeviceAutomationPicker<
|
||||
T extends DeviceAutomation,
|
||||
@@ -28,7 +30,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
|
||||
@property({ type: Object }) public value?: T;
|
||||
|
||||
@state() private _automations: T[] = [];
|
||||
@state() private _automations?: T[];
|
||||
|
||||
// Trigger an empty render so we start with a clean DOM.
|
||||
// paper-listbox does not like changing things around.
|
||||
@@ -44,12 +46,6 @@ export abstract class HaDeviceAutomationPicker<
|
||||
);
|
||||
}
|
||||
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
);
|
||||
}
|
||||
|
||||
private _localizeDeviceAutomation: (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
@@ -75,7 +71,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
if (!this.value) {
|
||||
if (!this.value || !this._automations) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -88,7 +84,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
);
|
||||
|
||||
if (idx === -1) {
|
||||
return UNKNOWN_AUTOMATION_KEY;
|
||||
return this.value.alias || this.value.type || "unknown";
|
||||
}
|
||||
|
||||
return `${this._automations[idx].device_id}_${idx}`;
|
||||
@@ -99,37 +95,21 @@ export abstract class HaDeviceAutomationPicker<
|
||||
return nothing;
|
||||
}
|
||||
const value = this._value;
|
||||
return html`
|
||||
<ha-md-select
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
@change=${this._automationChanged}
|
||||
@closed=${stopPropagation}
|
||||
.disabled=${this._automations.length === 0}
|
||||
>
|
||||
${value === NO_AUTOMATION_KEY
|
||||
? html`<ha-md-select-option .value=${NO_AUTOMATION_KEY}>
|
||||
${this.NO_AUTOMATION_TEXT}
|
||||
</ha-md-select-option>`
|
||||
: nothing}
|
||||
${value === UNKNOWN_AUTOMATION_KEY
|
||||
? html`<ha-md-select-option .value=${UNKNOWN_AUTOMATION_KEY}>
|
||||
${this.UNKNOWN_AUTOMATION_TEXT}
|
||||
</ha-md-select-option>`
|
||||
: nothing}
|
||||
${this._automations.map(
|
||||
(automation, idx) => html`
|
||||
<ha-md-select-option .value=${`${automation.device_id}_${idx}`}>
|
||||
${this._localizeDeviceAutomation(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
automation
|
||||
)}
|
||||
</ha-md-select-option>
|
||||
`
|
||||
)}
|
||||
</ha-md-select>
|
||||
`;
|
||||
|
||||
return html`<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
.disabled=${!this._automations || this._automations.length === 0}
|
||||
.getItems=${this._getItems(value, this._automations)}
|
||||
@value-changed=${this._automationChanged}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
)}
|
||||
hide-clear-icon
|
||||
>
|
||||
</ha-generic-picker>`;
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
@@ -140,6 +120,57 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
}
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(value: string, automations: T[] | undefined) => {
|
||||
if (!automations) {
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
const automationListItems = automations.map((automation, idx) => {
|
||||
const primary = this._localizeDeviceAutomation(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
automation
|
||||
);
|
||||
return {
|
||||
id: `${automation.device_id}_${idx}`,
|
||||
primary,
|
||||
};
|
||||
});
|
||||
|
||||
automationListItems.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(
|
||||
a.primary,
|
||||
b.primary,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
|
||||
if (value === NO_AUTOMATION_KEY) {
|
||||
automationListItems.unshift({
|
||||
id: NO_AUTOMATION_KEY,
|
||||
primary: this.NO_AUTOMATION_TEXT,
|
||||
});
|
||||
}
|
||||
|
||||
return () => automationListItems;
|
||||
}
|
||||
);
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||
const automation = this._automations?.find(
|
||||
(a, idx) => value === `${a.device_id}_${idx}`
|
||||
);
|
||||
|
||||
const text = automation
|
||||
? this._localizeDeviceAutomation(this.hass, this._entityReg, automation)
|
||||
: value === NO_AUTOMATION_KEY
|
||||
? this.NO_AUTOMATION_TEXT
|
||||
: value;
|
||||
|
||||
return html`<span slot="headline">${text}</span>`;
|
||||
};
|
||||
|
||||
private async _updateDeviceInfo() {
|
||||
this._automations = this.deviceId
|
||||
? (await this._fetchDeviceAutomations(this.hass, this.deviceId)).sort(
|
||||
@@ -161,13 +192,14 @@ export abstract class HaDeviceAutomationPicker<
|
||||
this._renderEmpty = false;
|
||||
}
|
||||
|
||||
private _automationChanged(ev) {
|
||||
const value = ev.target.value;
|
||||
if (!value || [UNKNOWN_AUTOMATION_KEY, NO_AUTOMATION_KEY].includes(value)) {
|
||||
private _automationChanged(ev: CustomEvent<{ value: string }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (!value || NO_AUTOMATION_KEY === value) {
|
||||
return;
|
||||
}
|
||||
const [deviceId, idx] = value.split("_");
|
||||
const automation = this._automations[idx];
|
||||
const automation = this._automations![idx];
|
||||
if (automation.device_id !== deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
|
||||
export type HaDevicePickerDeviceFilterFunc = (
|
||||
device: DeviceRegistryEntry
|
||||
@@ -94,7 +95,30 @@ export class HaDevicePicker extends LitElement {
|
||||
|
||||
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||
|
||||
private _getDevicesMemoized = memoizeOne(getDevices);
|
||||
private _getDevicesMemoized = memoizeOne(
|
||||
(
|
||||
_devices: HomeAssistant["devices"],
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeDevices?: string[],
|
||||
value?: string
|
||||
) =>
|
||||
getDevices(
|
||||
this.hass,
|
||||
configEntryLookup,
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeDevices,
|
||||
value
|
||||
)
|
||||
);
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
@@ -110,7 +134,7 @@ export class HaDevicePicker extends LitElement {
|
||||
|
||||
private _getItems = () =>
|
||||
this._getDevicesMemoized(
|
||||
this.hass,
|
||||
this.hass.devices,
|
||||
this._configEntryLookup,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
|
||||
@@ -18,10 +18,7 @@ import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-input-helper-text";
|
||||
import {
|
||||
NO_ITEMS_AVAILABLE_ID,
|
||||
type PickerComboBoxItem,
|
||||
} from "../ha-picker-combo-box";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import "../ha-sortable";
|
||||
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
@@ -184,18 +181,17 @@ export class HaEntityNamePicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.rowRenderer=${rowRenderer}
|
||||
.searchFn=${this._searchFn}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.no_match"
|
||||
)}
|
||||
.value=${this._getPickerValue()}
|
||||
allow-custom-value
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.custom_name"
|
||||
)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
.searchFn=${this._searchFn}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.search"
|
||||
)}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
<ha-sortable
|
||||
@@ -279,6 +275,11 @@ export class HaEntityNamePicker extends LitElement {
|
||||
this._editIndex = idx;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
const value = this._items[idx];
|
||||
// Pre-fill the field value when editing a text item
|
||||
if (value.type === "text" && value.text) {
|
||||
this._picker?.setFieldValue(value.text);
|
||||
}
|
||||
}
|
||||
|
||||
private get _items(): EntityNameItem[] {
|
||||
@@ -316,10 +317,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getFilteredItems = (
|
||||
searchString?: string,
|
||||
_section?: string
|
||||
): PickerComboBoxItem[] => {
|
||||
private _getFilteredItems = (): PickerComboBoxItem[] => {
|
||||
const items = this._getItems(this.entityId);
|
||||
const currentItem =
|
||||
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
||||
@@ -336,49 +334,27 @@ export class HaEntityNamePicker extends LitElement {
|
||||
);
|
||||
|
||||
// When editing an existing text item, include it in the base items
|
||||
if (currentItem?.type === "text" && currentItem.text && !searchString) {
|
||||
if (currentItem?.type === "text" && currentItem.text) {
|
||||
filteredItems.push(this._customNameOption(currentItem.text));
|
||||
}
|
||||
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
private _getAdditionalItems = (
|
||||
searchString?: string
|
||||
private _searchFn = (
|
||||
searchString: string,
|
||||
filteredItems: PickerComboBoxItem[]
|
||||
): PickerComboBoxItem[] => {
|
||||
if (!searchString) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentItem =
|
||||
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
||||
const currentId =
|
||||
currentItem?.type === "text" && currentItem.text
|
||||
? this._customNameOption(currentItem.text).id
|
||||
: undefined;
|
||||
|
||||
// Don't add if it's the same as the current item being edited
|
||||
if (
|
||||
currentItem?.type === "text" &&
|
||||
currentItem.text &&
|
||||
currentItem.text === searchString
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Always return custom name option when there's a search string
|
||||
// This prevents "No matching items found" from showing
|
||||
return [this._customNameOption(searchString)];
|
||||
};
|
||||
|
||||
private _searchFn = (
|
||||
search: string,
|
||||
filteredItems: PickerComboBoxItem[],
|
||||
_allItems: PickerComboBoxItem[]
|
||||
): PickerComboBoxItem[] => {
|
||||
// Remove NO_ITEMS_AVAILABLE_ID if we have additional items (custom name option)
|
||||
// This prevents "No matching items found" from showing when custom values are allowed
|
||||
const hasAdditionalItems = this._getAdditionalItems(search).length > 0;
|
||||
if (hasAdditionalItems) {
|
||||
return filteredItems.filter(
|
||||
(item) => typeof item !== "string" || item !== NO_ITEMS_AVAILABLE_ID
|
||||
);
|
||||
// Remove custom name option if search string is present to avoid duplicates
|
||||
if (searchString && currentId) {
|
||||
return filteredItems.filter((item) => item.id !== currentId);
|
||||
}
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import {
|
||||
STATE_DISPLAY_SPECIAL_CONTENT,
|
||||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS,
|
||||
@@ -19,7 +20,10 @@ import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-input-helper-text";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import {
|
||||
NO_ITEMS_AVAILABLE_ID,
|
||||
type PickerComboBoxItem,
|
||||
} from "../ha-picker-combo-box";
|
||||
import "../ha-sortable";
|
||||
|
||||
const HIDDEN_ATTRIBUTES = [
|
||||
@@ -92,6 +96,9 @@ export class HaStateContentPicker extends LitElement {
|
||||
@property({ type: Boolean, attribute: "allow-name" }) public allowName =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-context" }) public allowContext =
|
||||
false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string[] | string;
|
||||
@@ -103,7 +110,12 @@ export class HaStateContentPicker extends LitElement {
|
||||
private _editIndex?: number;
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
|
||||
(
|
||||
entityId?: string,
|
||||
stateObj?: HassEntity,
|
||||
allowName?: boolean,
|
||||
allowContext?: boolean
|
||||
) => {
|
||||
const domain = entityId ? computeDomain(entityId) : undefined;
|
||||
const items: PickerComboBoxItem[] = [
|
||||
{
|
||||
@@ -146,6 +158,52 @@ export class HaStateContentPicker extends LitElement {
|
||||
"ui.components.state-content-picker.last_updated"
|
||||
),
|
||||
},
|
||||
...(allowContext && stateObj
|
||||
? (() => {
|
||||
const context = getEntityContext(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
const contextItems: PickerComboBoxItem[] = [];
|
||||
if (context.device) {
|
||||
contextItems.push({
|
||||
id: "device_name",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.device_name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.device_name"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (context.area) {
|
||||
contextItems.push({
|
||||
id: "area_name",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.area_name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.area_name"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (context.floor) {
|
||||
contextItems.push({
|
||||
id: "floor_name",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.floor_name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.floor_name"
|
||||
),
|
||||
});
|
||||
}
|
||||
return contextItems;
|
||||
})()
|
||||
: []),
|
||||
...(domain
|
||||
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
|
||||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
|
||||
@@ -199,11 +257,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
.value=${this._getPickerValue()}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.notFoundLabel=${this.hass.localize("ui.components.combo-box.no_match")}
|
||||
allow-custom-value
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-state-content-picker.custom_state"
|
||||
)}
|
||||
.searchFn=${this._searchFn}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
@@ -301,7 +355,8 @@ export class HaStateContentPicker extends LitElement {
|
||||
const items = this._getItems(
|
||||
this.entityId,
|
||||
stateObjForItems,
|
||||
this.allowName
|
||||
this.allowName,
|
||||
this.allowContext
|
||||
);
|
||||
return items.find((item) => item.id === value)?.primary;
|
||||
}
|
||||
@@ -328,7 +383,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
(text: string): PickerComboBoxItem => ({
|
||||
id: text,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.entity.entity-state-content-picker.custom_state"
|
||||
"ui.components.entity.entity-state-content-picker.custom_attribute"
|
||||
),
|
||||
secondary: `"${text}"`,
|
||||
search_labels: {
|
||||
@@ -340,14 +395,16 @@ export class HaStateContentPicker extends LitElement {
|
||||
})
|
||||
);
|
||||
|
||||
private _getFilteredItems = (
|
||||
searchString?: string,
|
||||
_section?: string
|
||||
): PickerComboBoxItem[] => {
|
||||
private _getFilteredItems = (): PickerComboBoxItem[] => {
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
const items = this._getItems(this.entityId, stateObj, this.allowName);
|
||||
const items = this._getItems(
|
||||
this.entityId,
|
||||
stateObj,
|
||||
this.allowName,
|
||||
this.allowContext
|
||||
);
|
||||
const currentValue =
|
||||
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
||||
|
||||
@@ -358,11 +415,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
);
|
||||
|
||||
// When editing an existing custom value, include it in the base items
|
||||
if (
|
||||
currentValue &&
|
||||
!items.find((item) => item.id === currentValue) &&
|
||||
!searchString
|
||||
) {
|
||||
if (currentValue && !items.find((item) => item.id === currentValue)) {
|
||||
filteredItems.push(this._customValueOption(currentValue));
|
||||
}
|
||||
|
||||
@@ -372,33 +425,39 @@ export class HaStateContentPicker extends LitElement {
|
||||
private _getAdditionalItems = (
|
||||
searchString?: string
|
||||
): PickerComboBoxItem[] => {
|
||||
if (!searchString) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentValue =
|
||||
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
||||
|
||||
// Don't add if it's the same as the current item being edited
|
||||
if (currentValue && currentValue === searchString) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if the search string matches an existing item
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
const items = this._getItems(this.entityId, stateObj, this.allowName);
|
||||
const existingItem = items.find((item) => item.id === searchString);
|
||||
const items = this._getItems(
|
||||
this.entityId,
|
||||
stateObj,
|
||||
this.allowName,
|
||||
this.allowContext
|
||||
);
|
||||
|
||||
// Only return custom value option if it doesn't match an existing item
|
||||
if (!existingItem) {
|
||||
// If the search string does not match with the id of any of the items,
|
||||
// offer to add it as a custom attribute
|
||||
const existingItem = items.find((item) => item.id === searchString);
|
||||
if (searchString && !existingItem) {
|
||||
return [this._customValueOption(searchString)];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
private _searchFn = (
|
||||
search: string,
|
||||
filteredItems: PickerComboBoxItem[],
|
||||
_allItems: PickerComboBoxItem[]
|
||||
): PickerComboBoxItem[] => {
|
||||
if (!search) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
// Always exclude NO_ITEMS_AVAILABLE_ID (since custom values are allowed) and currentValue (the custom value being edited)
|
||||
return filteredItems.filter((item) => item.id !== NO_ITEMS_AVAILABLE_ID);
|
||||
};
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getStates } from "../../common/entity/get_states";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
@customElement("ha-entity-state-picker")
|
||||
export class HaEntityStatePicker extends LitElement {
|
||||
@@ -108,6 +109,12 @@ export class HaEntityStatePicker extends LitElement {
|
||||
this.extraOptions
|
||||
);
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||
const items = this._getFilteredItems();
|
||||
const item = items.find((option) => option.id === value);
|
||||
return html`<span slot="headline">${item?.primary ?? value}</span>`;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
@@ -125,6 +132,7 @@ export class HaEntityStatePicker extends LitElement {
|
||||
.helper=${this.helper}
|
||||
.value=${this.value}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.notFoundLabel=${this.hass.localize("ui.components.combo-box.no_match")}
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-state-picker.add_custom_state"
|
||||
|
||||
@@ -143,17 +143,19 @@ export class HaEntityToggle extends LitElement {
|
||||
// Optimistic update.
|
||||
this._isOn = turnOn;
|
||||
|
||||
await this.hass.callService(serviceDomain, service, {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
// If after 2 seconds we have not received a state update
|
||||
// reset the switch to it's original state.
|
||||
if (this.stateObj === currentState) {
|
||||
this._isOn = isOn(this.stateObj);
|
||||
}
|
||||
}, 2000);
|
||||
try {
|
||||
await this.hass.callService(serviceDomain, service, {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
} finally {
|
||||
setTimeout(async () => {
|
||||
// If after 2 seconds we have not received a state update
|
||||
// reset the switch to it's original state.
|
||||
if (this.stateObj === currentState) {
|
||||
this._isOn = isOn(this.stateObj);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -141,6 +141,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
|
||||
private async _getStatisticIds() {
|
||||
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
|
||||
this._picker?.requestUpdate();
|
||||
}
|
||||
|
||||
private _getItems = () =>
|
||||
@@ -177,9 +178,9 @@ export class HaStatisticPicker extends LitElement {
|
||||
entitiesOnly?: boolean,
|
||||
excludeStatistics?: string[],
|
||||
value?: string
|
||||
): StatisticComboBoxItem[] => {
|
||||
): StatisticComboBoxItem[] | undefined => {
|
||||
if (!statisticIds) {
|
||||
return [];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (includeStatisticsUnitOfMeasurement) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
@@ -88,6 +88,9 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
@property({ type: Boolean, attribute: "block-mode-change" })
|
||||
public blockModeChange = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "without-header" })
|
||||
public withoutHeader = false;
|
||||
|
||||
@state() private _mode: DialogSheetMode = "dialog";
|
||||
|
||||
private _unsubMediaQuery?: () => void;
|
||||
@@ -118,27 +121,33 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
if (this._mode === "bottom-sheet") {
|
||||
return html`
|
||||
<ha-bottom-sheet .open=${this.open} flexcontent>
|
||||
<ha-dialog-header
|
||||
slot="header"
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-drawer="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
${!this.withoutHeader
|
||||
? html`<ha-dialog-header
|
||||
slot="header"
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-drawer="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span
|
||||
slot="title"
|
||||
class="title"
|
||||
id="ha-wa-dialog-title"
|
||||
>
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>`
|
||||
: nothing}
|
||||
<slot></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</ha-bottom-sheet>
|
||||
@@ -156,6 +165,7 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
.headerSubtitle=${this.headerSubtitle}
|
||||
.headerSubtitlePosition=${this.headerSubtitlePosition}
|
||||
flexcontent
|
||||
.withoutHeader=${this.withoutHeader}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="headerNavigationIcon">
|
||||
<ha-icon-button
|
||||
|
||||
@@ -57,13 +57,13 @@ class HaAddonPicker extends LitElement {
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._getAddons();
|
||||
this._getApps();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const label =
|
||||
this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.addon-picker.addon")
|
||||
? this.hass.localize("ui.components.app-picker.app")
|
||||
: this.label;
|
||||
|
||||
if (this._error) {
|
||||
@@ -92,7 +92,7 @@ class HaAddonPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _getAddons() {
|
||||
private async _getApps() {
|
||||
try {
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
@@ -113,12 +113,12 @@ class HaAddonPicker extends LitElement {
|
||||
}));
|
||||
} else {
|
||||
this._error = this.hass.localize(
|
||||
"ui.components.addon-picker.error.no_supervisor"
|
||||
"ui.components.app-picker.error.no_supervisor"
|
||||
);
|
||||
}
|
||||
} catch (_err: any) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.components.addon-picker.error.fetch_addons"
|
||||
"ui.components.app-picker.error.fetch_apps"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
342
src/components/ha-area-controls-picker.ts
Normal file
342
src/components/ha-area-controls-picker.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import Fuse from "fuse.js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeEntityNameList } from "../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
} from "../resources/fuseMultiTerm";
|
||||
import {
|
||||
AREA_CONTROLS_BUTTONS,
|
||||
getAreaControlEntities,
|
||||
type AreaControlDomain,
|
||||
} from "../data/area/area_controls";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-generic-picker";
|
||||
import "./ha-state-icon";
|
||||
|
||||
export interface AreaControlPickerItem extends PickerComboBoxItem {
|
||||
type?: "domain" | "entity";
|
||||
stateObj?: HassEntity;
|
||||
domain?: string;
|
||||
deviceClass?: string;
|
||||
}
|
||||
|
||||
const AREA_CONTROL_DOMAINS: readonly AreaControlDomain[] = [
|
||||
"light",
|
||||
"fan",
|
||||
"switch",
|
||||
"cover-shutter",
|
||||
"cover-blind",
|
||||
"cover-curtain",
|
||||
"cover-shade",
|
||||
"cover-awning",
|
||||
"cover-garage",
|
||||
"cover-gate",
|
||||
"cover-door",
|
||||
"cover-window",
|
||||
"cover-damper",
|
||||
] as const;
|
||||
|
||||
@customElement("ha-area-controls-picker")
|
||||
export class HaAreaControlsPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "area-id" }) public areaId!: string;
|
||||
|
||||
@property({ type: Array, attribute: "exclude-entities" })
|
||||
public excludeEntities?: string[];
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property({ type: Array, attribute: "exclude-values" })
|
||||
public excludeValues?: string[];
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||
|
||||
private _domainSearchKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
];
|
||||
|
||||
private _entitySearchKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "secondary",
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
name: "id",
|
||||
weight: 3,
|
||||
},
|
||||
];
|
||||
|
||||
private _createFuseIndex = (
|
||||
items: AreaControlPickerItem[],
|
||||
keys: FuseWeightedKey[]
|
||||
) => Fuse.createIndex(keys, items);
|
||||
|
||||
private _domainFuseIndex = memoizeOne((items: AreaControlPickerItem[]) =>
|
||||
this._createFuseIndex(items, this._domainSearchKeys)
|
||||
);
|
||||
|
||||
private _entityFuseIndex = memoizeOne((items: AreaControlPickerItem[]) =>
|
||||
this._createFuseIndex(items, this._entitySearchKeys)
|
||||
);
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
areaId: string,
|
||||
excludeEntities: string[] | undefined,
|
||||
currentValue: string | undefined,
|
||||
excludeValues: string[] | undefined,
|
||||
localize: LocalizeFunc,
|
||||
_entities: HomeAssistant["entities"],
|
||||
_devices: HomeAssistant["devices"],
|
||||
_areas: HomeAssistant["areas"]
|
||||
): ((
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (AreaControlPickerItem | string)[]) =>
|
||||
(searchString?: string, section?: string) => {
|
||||
if (!this.hass) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isSelected = (id: string): boolean =>
|
||||
currentValue === id ||
|
||||
(excludeValues !== undefined && excludeValues.includes(id));
|
||||
|
||||
const controlEntities = getAreaControlEntities(
|
||||
AREA_CONTROL_DOMAINS as unknown as AreaControlDomain[],
|
||||
areaId,
|
||||
excludeEntities,
|
||||
this.hass
|
||||
);
|
||||
|
||||
const items: (AreaControlPickerItem | string)[] = [];
|
||||
let domainItems: AreaControlPickerItem[] = [];
|
||||
let entityItems: AreaControlPickerItem[] = [];
|
||||
|
||||
if (!section || section === "domain") {
|
||||
const supportedControls = (
|
||||
Object.keys(controlEntities) as (keyof typeof controlEntities)[]
|
||||
).filter((control) => controlEntities[control].length > 0);
|
||||
|
||||
supportedControls.forEach((control) => {
|
||||
if (isSelected(control)) {
|
||||
return;
|
||||
}
|
||||
const label = localize(
|
||||
`ui.panel.lovelace.editor.features.types.area-controls.controls_options.${control}`
|
||||
);
|
||||
const button = AREA_CONTROLS_BUTTONS[control];
|
||||
const deviceClass = button.filter.device_class
|
||||
? Array.isArray(button.filter.device_class)
|
||||
? button.filter.device_class[0]
|
||||
: button.filter.device_class
|
||||
: undefined;
|
||||
|
||||
domainItems.push({
|
||||
type: "domain",
|
||||
id: control,
|
||||
primary: label,
|
||||
domain: button.filter.domain,
|
||||
deviceClass,
|
||||
});
|
||||
});
|
||||
|
||||
if (searchString) {
|
||||
const fuseIndex = this._domainFuseIndex(domainItems);
|
||||
domainItems = multiTermSortedSearch(
|
||||
domainItems,
|
||||
searchString,
|
||||
this._domainSearchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!section || section === "entity") {
|
||||
const allEntityIds = Object.values(controlEntities).flat();
|
||||
const uniqueEntityIds = Array.from(new Set(allEntityIds));
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
uniqueEntityIds.forEach((entityId) => {
|
||||
if (isSelected(entityId)) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass!.states[entityId];
|
||||
if (!stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass!.entities,
|
||||
this.hass!.devices,
|
||||
this.hass!.areas,
|
||||
this.hass!.floors
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
entityItems.push({
|
||||
type: "entity",
|
||||
id: entityId,
|
||||
primary,
|
||||
secondary,
|
||||
stateObj,
|
||||
});
|
||||
});
|
||||
|
||||
if (searchString) {
|
||||
const fuseIndex = this._entityFuseIndex(entityItems);
|
||||
entityItems = multiTermSortedSearch(
|
||||
entityItems,
|
||||
searchString,
|
||||
this._entitySearchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Only add section headers if there are items in that section
|
||||
if (!section) {
|
||||
if (domainItems.length > 0) {
|
||||
items.push(
|
||||
localize(
|
||||
"ui.panel.lovelace.editor.features.types.area-controls.sections.domain"
|
||||
)
|
||||
);
|
||||
items.push(...domainItems);
|
||||
}
|
||||
if (entityItems.length > 0) {
|
||||
items.push(
|
||||
localize(
|
||||
"ui.panel.lovelace.editor.features.types.area-controls.sections.entity"
|
||||
)
|
||||
);
|
||||
items.push(...entityItems);
|
||||
}
|
||||
} else {
|
||||
items.push(...domainItems, ...entityItems);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _rowRenderer = (item: AreaControlPickerItem) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
${item.type === "entity" && item.stateObj
|
||||
? html`<ha-state-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${item.stateObj}
|
||||
></ha-state-icon>`
|
||||
: item.domain
|
||||
? html`<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${item.domain}
|
||||
.deviceClass=${item.deviceClass}
|
||||
></ha-domain-icon>`
|
||||
: nothing}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${item.type === "entity" && item.stateObj
|
||||
? html`<span slot="supporting-text" class="code">
|
||||
${item.stateObj.entity_id}
|
||||
</span>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value || ""}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.helper=${this.helper}
|
||||
.addButtonLabel=${this.addButtonLabel}
|
||||
.getItems=${this._getItems(
|
||||
this.areaId,
|
||||
this.excludeEntities,
|
||||
this.value,
|
||||
this.excludeValues,
|
||||
this.hass.localize,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas
|
||||
)}
|
||||
.rowRenderer=${this._rowRenderer as any}
|
||||
.sections=${[
|
||||
{
|
||||
id: "domain",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.area-controls.sections.domain"
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "entity",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.area-controls.sections.entity"
|
||||
),
|
||||
},
|
||||
]}
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.code {
|
||||
font-family: var(--ha-font-family-code);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-area-controls-picker": HaAreaControlsPicker;
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,10 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import { createAreaRegistryEntry } from "../data/area_registry";
|
||||
import type {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
} from "../data/device/device_registry";
|
||||
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
|
||||
import { areaComboBoxKeys, getAreas } from "../data/area/area_picker";
|
||||
import { createAreaRegistryEntry } from "../data/area/area_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
@@ -30,12 +24,6 @@ import "./ha-svg-icon";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "search_labels.areaName", weight: 10 },
|
||||
{ name: "search_labels.aliases", weight: 8 },
|
||||
{ name: "search_labels.floorName", weight: 6 },
|
||||
{ name: "search_labels.id", weight: 3 },
|
||||
];
|
||||
@customElement("ha-area-picker")
|
||||
export class HaAreaPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -102,6 +90,8 @@ export class HaAreaPicker extends LitElement {
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private _getAreasMemoized = memoizeOne(getAreas);
|
||||
|
||||
// Recompute value renderer when the areas change
|
||||
private _computeValueRenderer = memoizeOne(
|
||||
(_haAreas: HomeAssistant["areas"]): PickerValueRenderer =>
|
||||
@@ -137,183 +127,13 @@ export class HaAreaPicker extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getAreas = memoizeOne(
|
||||
(
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
excludeAreas: this["excludeAreas"]
|
||||
): PickerComboBoxItem[] => {
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
const areas = Object.values(haAreas);
|
||||
const devices = Object.values(haDevices);
|
||||
const entities = Object.values(haEntities);
|
||||
|
||||
if (
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses ||
|
||||
deviceFilter ||
|
||||
entityFilter
|
||||
) {
|
||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) =>
|
||||
deviceFilter!(device)
|
||||
);
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter(stateObj);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter!(stateObj);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outputAreas = areas;
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
areaIds = inputDevices
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.area_id!);
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
areaIds = (areaIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.area_id)
|
||||
.map((entity) => entity.area_id!)
|
||||
);
|
||||
}
|
||||
|
||||
if (areaIds) {
|
||||
outputAreas = outputAreas.filter((area) =>
|
||||
areaIds!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeAreas) {
|
||||
outputAreas = outputAreas.filter(
|
||||
(area) => !excludeAreas!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
const items = outputAreas.map<PickerComboBoxItem>((area) => {
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
||||
const areaName = computeAreaName(area);
|
||||
return {
|
||||
id: area.area_id,
|
||||
primary: areaName || area.area_id,
|
||||
secondary: floorName,
|
||||
icon: area.icon || undefined,
|
||||
icon_path: area.icon ? undefined : mdiTextureBox,
|
||||
search_labels: {
|
||||
areaName: areaName || null,
|
||||
floorName: floorName || null,
|
||||
id: area.area_id,
|
||||
aliases: area.aliases.join(" "),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = () =>
|
||||
this._getAreas(
|
||||
this._getAreasMemoized(
|
||||
this.hass.areas,
|
||||
this.hass.floors,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this.hass.states,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
@@ -394,7 +214,7 @@ export class HaAreaPicker extends LitElement {
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.addButtonLabel=${this.addButtonLabel}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.searchKeys=${areaComboBoxKeys}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.area-picker.unknown"
|
||||
)}
|
||||
|
||||
@@ -174,12 +174,14 @@ export class HaAutomationRow extends LitElement {
|
||||
}
|
||||
::slotted([slot="header"]) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
margin: 0 var(--ha-space-3);
|
||||
}
|
||||
.icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
:host([sort-selected]) .row {
|
||||
outline: solid;
|
||||
|
||||
@@ -208,7 +208,8 @@ export class HaBaseTimeInput extends LitElement {
|
||||
? html`<ha-textfield
|
||||
id="sec"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
inputmode="decimal"
|
||||
step="any"
|
||||
.value=${this._formatValue(this.seconds)}
|
||||
.label=${this.secLabel}
|
||||
@change=${this._valueChanged}
|
||||
@@ -217,7 +218,6 @@ export class HaBaseTimeInput extends LitElement {
|
||||
no-spinner
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.autoValidate}
|
||||
maxlength="2"
|
||||
max="59"
|
||||
min="0"
|
||||
.disabled=${this.disabled}
|
||||
@@ -311,7 +311,8 @@ export class HaBaseTimeInput extends LitElement {
|
||||
* Format time fragments
|
||||
*/
|
||||
private _formatValue(value: number, padding = 2) {
|
||||
return value.toString().padStart(padding, "0");
|
||||
const str = value.toString();
|
||||
return str.includes(".") ? str : str.padStart(padding, "0");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,7 +51,10 @@ export class HaCard extends LitElement {
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
|
||||
:host ::slotted(.card-content:not(:first-child)),
|
||||
:host
|
||||
::slotted(
|
||||
.card-content:not(:nth-child(1 of .card-content, .card-header))
|
||||
),
|
||||
slot:not(:first-child)::slotted(.card-content) {
|
||||
padding-top: 0;
|
||||
margin-top: calc(var(--ha-space-2) * -1);
|
||||
|
||||
@@ -255,6 +255,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
...this._loadedCodeMirror.tabKeyBindings,
|
||||
saveKeyBinding,
|
||||
]),
|
||||
this._loadedCodeMirror.search({ top: true }),
|
||||
this._loadedCodeMirror.langCompartment.of(this._mode),
|
||||
this._loadedCodeMirror.haTheme,
|
||||
this._loadedCodeMirror.haSyntaxHighlighting,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-combo-box-textfield")
|
||||
export class HaComboBoxTextField extends HaTextField {
|
||||
@property({ type: Boolean, attribute: "force-blank-value" })
|
||||
public forceBlankValue = false;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("value") || changedProps.has("forceBlankValue")) {
|
||||
if (this.forceBlankValue && this.value) {
|
||||
this.value = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-combo-box-textfield": HaComboBoxTextField;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { mdiMinusThick, mdiPlusThick } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
import "./ha-button-toggle-group";
|
||||
|
||||
export interface HaDurationData {
|
||||
days?: number;
|
||||
@@ -13,6 +15,8 @@ export interface HaDurationData {
|
||||
milliseconds?: number;
|
||||
}
|
||||
|
||||
const FIELDS = ["milliseconds", "seconds", "minutes", "hours", "days"];
|
||||
|
||||
@customElement("ha-duration-input")
|
||||
class HaDurationInput extends LitElement {
|
||||
@property({ attribute: false }) public data?: HaDurationData;
|
||||
@@ -29,41 +33,80 @@ class HaDurationInput extends LitElement {
|
||||
@property({ attribute: "enable-day", type: Boolean })
|
||||
public enableDay = false;
|
||||
|
||||
@property({ attribute: "allow-negative", type: Boolean })
|
||||
public allowNegative = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
private _toggleNegative = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-base-time-input
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
.clearable=${!this.required && this.data !== undefined}
|
||||
.autoValidate=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
errorMessage="Required"
|
||||
enable-second
|
||||
.enableMillisecond=${this.enableMillisecond}
|
||||
.enableDay=${this.enableDay}
|
||||
format="24"
|
||||
.days=${this._days}
|
||||
.hours=${this._hours}
|
||||
.minutes=${this._minutes}
|
||||
.seconds=${this._seconds}
|
||||
.milliseconds=${this._milliseconds}
|
||||
@value-changed=${this._durationChanged}
|
||||
no-hours-limit
|
||||
day-label="dd"
|
||||
hour-label="hh"
|
||||
min-label="mm"
|
||||
sec-label="ss"
|
||||
ms-label="ms"
|
||||
></ha-base-time-input>
|
||||
<div class="row">
|
||||
${this.allowNegative
|
||||
? html`
|
||||
<ha-button-toggle-group
|
||||
size="small"
|
||||
.buttons=${[
|
||||
{ label: "+", iconPath: mdiPlusThick, value: "+" },
|
||||
{ label: "-", iconPath: mdiMinusThick, value: "-" },
|
||||
]}
|
||||
.active=${this._negative ? "-" : "+"}
|
||||
@value-changed=${this._negativeChanged}
|
||||
></ha-button-toggle-group>
|
||||
`
|
||||
: nothing}
|
||||
<ha-base-time-input
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
.clearable=${!this.required && this.data !== undefined}
|
||||
.autoValidate=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
errorMessage="Required"
|
||||
enable-second
|
||||
.enableMillisecond=${this.enableMillisecond}
|
||||
.enableDay=${this.enableDay}
|
||||
format="24"
|
||||
.days=${this._days}
|
||||
.hours=${this._hours}
|
||||
.minutes=${this._minutes}
|
||||
.seconds=${this._seconds}
|
||||
.milliseconds=${this._milliseconds}
|
||||
@value-changed=${this._durationChanged}
|
||||
no-hours-limit
|
||||
day-label="dd"
|
||||
hour-label="hh"
|
||||
min-label="mm"
|
||||
sec-label="ss"
|
||||
ms-label="ms"
|
||||
></ha-base-time-input>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _negative() {
|
||||
return (
|
||||
this._toggleNegative ||
|
||||
(this.data?.days
|
||||
? this.data.days < 0
|
||||
: this.data?.hours
|
||||
? this.data.hours < 0
|
||||
: this.data?.minutes
|
||||
? this.data.minutes < 0
|
||||
: this.data?.seconds
|
||||
? this.data.seconds < 0
|
||||
: this.data?.milliseconds
|
||||
? this.data.milliseconds < 0
|
||||
: false)
|
||||
);
|
||||
}
|
||||
|
||||
private get _days() {
|
||||
return this.data?.days
|
||||
? Number(this.data.days)
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.days))
|
||||
: Number(this.data.days)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -71,7 +114,9 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
private get _hours() {
|
||||
return this.data?.hours
|
||||
? Number(this.data.hours)
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.hours))
|
||||
: Number(this.data.hours)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -79,7 +124,9 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
private get _minutes() {
|
||||
return this.data?.minutes
|
||||
? Number(this.data.minutes)
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.minutes))
|
||||
: Number(this.data.minutes)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -87,7 +134,9 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
private get _seconds() {
|
||||
return this.data?.seconds
|
||||
? Number(this.data.seconds)
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.seconds))
|
||||
: Number(this.data.seconds)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -95,7 +144,9 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
private get _milliseconds() {
|
||||
return this.data?.milliseconds
|
||||
? Number(this.data.milliseconds)
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.milliseconds))
|
||||
: Number(this.data.milliseconds)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -113,6 +164,14 @@ class HaDurationInput extends LitElement {
|
||||
if ("days" in value) value.days ||= 0;
|
||||
if ("milliseconds" in value) value.milliseconds ||= 0;
|
||||
|
||||
if (this.allowNegative) {
|
||||
FIELDS.forEach((t) => {
|
||||
if (value[t]) {
|
||||
value[t] = Math.abs(value[t]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.enableMillisecond && !value.milliseconds) {
|
||||
// @ts-ignore
|
||||
delete value.milliseconds;
|
||||
@@ -135,12 +194,47 @@ class HaDurationInput extends LitElement {
|
||||
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
|
||||
value.hours %= 24;
|
||||
}
|
||||
|
||||
if (this._negative) {
|
||||
FIELDS.forEach((t) => {
|
||||
if (value[t]) {
|
||||
value[t] = -Math.abs(value[t]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
private _negativeChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const negative = (ev.detail?.value || ev.target.value) === "-";
|
||||
this._toggleNegative = negative;
|
||||
const value = this.data;
|
||||
if (value) {
|
||||
FIELDS.forEach((t) => {
|
||||
if (value[t]) {
|
||||
value[t] = negative ? -Math.abs(value[t]) : Math.abs(value[t]);
|
||||
}
|
||||
});
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
ha-button-toggle-group {
|
||||
margin: var(--ha-space-2);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ActionDetail, SelectedDetail } from "@material/mwc-list";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
@@ -25,7 +25,8 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { showCategoryRegistryDetailDialog } from "../panels/config/category/show-dialog-category-registry-detail";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-button-menu";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-list";
|
||||
@@ -114,37 +115,36 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`}
|
||||
${category.name}
|
||||
<ha-button-menu
|
||||
<ha-dropdown
|
||||
@click=${stopPropagation}
|
||||
@action=${this._handleAction}
|
||||
@wa-select=${this._handleAction}
|
||||
slot="meta"
|
||||
fixed
|
||||
.categoryId=${category.category_id}
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon"
|
||||
><ha-svg-icon
|
||||
<ha-dropdown-item value="edit">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiPencil}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.category.editor.edit"
|
||||
)}</ha-list-item
|
||||
>
|
||||
<ha-list-item graphic="icon" class="warning"
|
||||
><ha-svg-icon
|
||||
class="warning"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="delete" variant="danger">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDelete}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.category.editor.delete"
|
||||
)}</ha-list-item
|
||||
>
|
||||
</ha-button-menu>
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-list>
|
||||
@@ -174,13 +174,14 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
private _handleAction(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
const categoryId = (ev.currentTarget as any).categoryId;
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
const action = ev.detail.item.value;
|
||||
switch (action) {
|
||||
case "edit":
|
||||
this._editCategory(categoryId);
|
||||
break;
|
||||
case 1:
|
||||
case "delete":
|
||||
this._deleteCategory(categoryId);
|
||||
break;
|
||||
}
|
||||
@@ -316,6 +317,9 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
--mdc-list-side-padding-right: 4px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
}
|
||||
ha-dropdown-item {
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
198
src/components/ha-filter-voice-assistants.ts
Normal file
198
src/components/ha-filter-voice-assistants.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-label";
|
||||
import "./ha-list";
|
||||
import "./ha-list-item";
|
||||
import "./voice-assistant-brand-icon";
|
||||
import { voiceAssistants } from "../data/expose";
|
||||
import "../panels/config/voice-assistants/expose/expose-assistant-icon";
|
||||
|
||||
@customElement("ha-filter-voice-assistants")
|
||||
export class HaFilterVoiceAssistants extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// the list of selected voiceAssistantIds
|
||||
@property({ attribute: false }) public value: string[] = [];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _voiceAssistantOptions: string[] = [];
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.dashboard.voice_assistants.main"
|
||||
)}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
@click=${this._clearFilter}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<ha-list
|
||||
@selected=${this._assistantsSelected}
|
||||
class="ha-scrollbar"
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._voiceAssistantOptions,
|
||||
(voiceAssistantId) => voiceAssistantId,
|
||||
(voiceAssistantId) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${voiceAssistantId}
|
||||
.selected=${(this.value || []).includes(voiceAssistantId)}
|
||||
hasMeta
|
||||
graphic="icon"
|
||||
>
|
||||
<voice-assistant-brand-icon
|
||||
slot="graphic"
|
||||
.voiceAssistantId=${voiceAssistantId}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistant-brand-icon>
|
||||
${voiceAssistants[voiceAssistantId].name}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list> `
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._voiceAssistantOptions = Object.keys(voiceAssistants);
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
`${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private async _assistantsSelected(
|
||||
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||
) {
|
||||
if (!ev.detail.index) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const newvalue: string[] = [];
|
||||
for (const index of ev.detail.index) {
|
||||
newvalue.push(this._voiceAssistantOptions![index]);
|
||||
}
|
||||
this.value = newvalue;
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _clearFilter(ev) {
|
||||
ev.preventDefault();
|
||||
this.value = [];
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
.add {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-voice-assistants": HaFilterVoiceAssistants;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { updateAreaRegistryEntry } from "../data/area_registry";
|
||||
import { updateAreaRegistryEntry } from "../data/area/area_registry";
|
||||
import type {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import type { Selector } from "../../data/selector";
|
||||
import type { HaFormSchema } from "./types";
|
||||
import type { HaFormData, HaFormSchema } from "./types";
|
||||
|
||||
const setDefaultValue = (
|
||||
field: HaFormSchema,
|
||||
value: HaFormData | undefined
|
||||
) => {
|
||||
if ("selector" in field && "choose" in field.selector) {
|
||||
const firstChoice = Object.keys(field.selector.choose.choices)[0];
|
||||
if (firstChoice) {
|
||||
return {
|
||||
active_choice: firstChoice,
|
||||
[firstChoice]: value,
|
||||
};
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export const computeInitialHaFormData = (
|
||||
schema: HaFormSchema[] | readonly HaFormSchema[]
|
||||
@@ -10,9 +26,12 @@ export const computeInitialHaFormData = (
|
||||
field.description?.suggested_value !== undefined &&
|
||||
field.description?.suggested_value !== null
|
||||
) {
|
||||
data[field.name] = field.description.suggested_value;
|
||||
data[field.name] = setDefaultValue(
|
||||
field,
|
||||
field.description.suggested_value
|
||||
);
|
||||
} else if ("default" in field) {
|
||||
data[field.name] = field.default;
|
||||
data[field.name] = setDefaultValue(field, field.default);
|
||||
} else if (field.type === "expandable") {
|
||||
const expandableData = computeInitialHaFormData(field.schema);
|
||||
if (field.required || Object.keys(expandableData).length) {
|
||||
@@ -108,6 +127,21 @@ export const computeInitialHaFormData = (
|
||||
data[field.name] = {};
|
||||
} else if ("state" in selector) {
|
||||
data[field.name] = selector.state?.multiple ? [] : "";
|
||||
} else if ("choose" in selector) {
|
||||
const firstChoice = Object.keys(selector.choose.choices)[0];
|
||||
if (!firstChoice) {
|
||||
data[field.name] = {};
|
||||
} else {
|
||||
data[field.name] = {
|
||||
active_choice: firstChoice,
|
||||
[firstChoice]: computeInitialHaFormData([
|
||||
{
|
||||
name: firstChoice,
|
||||
selector: selector.choose.choices[firstChoice].selector,
|
||||
},
|
||||
])[firstChoice],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
|
||||
|
||||
@@ -7,7 +7,8 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-list-item";
|
||||
import "../ha-dropdown";
|
||||
import "../ha-dropdown-item";
|
||||
import "../ha-svg-icon";
|
||||
import "./ha-form";
|
||||
import type {
|
||||
@@ -116,9 +117,8 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
: nothing}
|
||||
${hiddenActions.length > 0
|
||||
? html`
|
||||
<ha-button-menu
|
||||
@action=${this._handleAddAction}
|
||||
fixed
|
||||
<ha-dropdown
|
||||
@wa-select=${this._handleAddAction}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-button slot="trigger" appearance="filled" size="small">
|
||||
@@ -129,26 +129,21 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
${hiddenActions.map((action) => {
|
||||
const actionSchema = schemaMap.get(action);
|
||||
return html`
|
||||
<ha-list-item>
|
||||
<ha-dropdown-item .value=${action}>
|
||||
${this.computeLabel && actionSchema
|
||||
? this.computeLabel(actionSchema)
|
||||
: action}
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
`;
|
||||
})}
|
||||
</ha-button-menu>
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAddAction(ev: CustomEvent) {
|
||||
const hiddenActions = this._hiddenActions(
|
||||
this.schema.schema,
|
||||
this._displayActions ?? NO_ACTIONS
|
||||
);
|
||||
const index = ev.detail.index;
|
||||
const action = hiddenActions[index];
|
||||
private _handleAddAction(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
const action = ev.detail.item.value;
|
||||
this._displayActions = [...(this._displayActions ?? []), action];
|
||||
}
|
||||
|
||||
@@ -161,6 +156,9 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
:host ha-form {
|
||||
display: block;
|
||||
}
|
||||
ha-dropdown {
|
||||
display: inline-block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiPlaylistPlus } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type CSSResultGroup,
|
||||
type PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { throttle } from "../common/util/throttle";
|
||||
import { PickerMixin } from "../mixins/picker-mixin";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -39,7 +46,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
public getItems!: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
) => (PickerComboBoxItem | string)[] | undefined;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
@@ -114,6 +121,8 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
|
||||
@state() private _openedNarrow = false;
|
||||
|
||||
@state() private _unknownValue = false;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
@@ -130,6 +139,25 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
|
||||
private _unsubscribeTinyKeys?: () => void;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("value")) {
|
||||
this._setUnknownValue();
|
||||
return;
|
||||
}
|
||||
if (changedProperties.has("hass")) {
|
||||
this._throttleUnknownValue();
|
||||
}
|
||||
}
|
||||
|
||||
public setFieldValue(value: string) {
|
||||
if (this._comboBox) {
|
||||
this._comboBox.setFieldValue(value);
|
||||
return;
|
||||
}
|
||||
// Store initial value to set when opened
|
||||
this._initialFieldValue = value;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
// Only show label if it's not a top label and there is a value.
|
||||
const label = this.useTopLabel && this.value ? undefined : this.label;
|
||||
@@ -157,11 +185,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
type="button"
|
||||
class=${this._opened ? "opened" : ""}
|
||||
compact
|
||||
.unknown=${this._unknownValue(
|
||||
this.allowCustomValue,
|
||||
this.value,
|
||||
this.getItems()
|
||||
)}
|
||||
.unknown=${this._unknownValue}
|
||||
.unknownItemText=${this.unknownItemText}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@click=${this.open}
|
||||
@@ -182,40 +206,42 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
</ha-picker-field>`}
|
||||
</slot>
|
||||
</div>
|
||||
${!this._openedNarrow && (this._pickerWrapperOpen || this._opened)
|
||||
? html`
|
||||
<wa-popover
|
||||
.open=${this._pickerWrapperOpen}
|
||||
style="--body-width: ${this._popoverWidth}px;"
|
||||
without-arrow
|
||||
distance="-4"
|
||||
.placement=${this.popoverPlacement}
|
||||
for="picker"
|
||||
auto-size="vertical"
|
||||
auto-size-padding="16"
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@wa-after-hide=${this._hidePicker}
|
||||
trap-focus
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.label || "Select option"}
|
||||
>
|
||||
${this._renderComboBox()}
|
||||
</wa-popover>
|
||||
`
|
||||
: this._pickerWrapperOpen || this._opened
|
||||
? html`<ha-bottom-sheet
|
||||
flexcontent
|
||||
.open=${this._pickerWrapperOpen}
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@closed=${this._hidePicker}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.label || "Select option"}
|
||||
>
|
||||
${this._renderComboBox(true)}
|
||||
</ha-bottom-sheet>`
|
||||
: nothing}
|
||||
${this._pickerWrapperOpen || this._opened
|
||||
? this._openedNarrow
|
||||
? html`
|
||||
<ha-bottom-sheet
|
||||
flexcontent
|
||||
.open=${this._pickerWrapperOpen}
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@closed=${this._hidePicker}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.label || "Select option"}
|
||||
>
|
||||
${this._renderComboBox(true)}
|
||||
</ha-bottom-sheet>
|
||||
`
|
||||
: html`
|
||||
<wa-popover
|
||||
.open=${this._pickerWrapperOpen}
|
||||
style="--body-width: ${this._popoverWidth}px;"
|
||||
without-arrow
|
||||
distance="-4"
|
||||
.placement=${this.popoverPlacement}
|
||||
for="picker"
|
||||
auto-size="vertical"
|
||||
auto-size-padding="16"
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@wa-after-hide=${this._hidePicker}
|
||||
trap-focus
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.label || "Select option"}
|
||||
>
|
||||
${this._renderComboBox()}
|
||||
</wa-popover>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._renderHelper()}`;
|
||||
}
|
||||
@@ -248,26 +274,29 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _unknownValue = memoizeOne(
|
||||
(
|
||||
allowCustomValue: boolean,
|
||||
value?: string,
|
||||
items?: (PickerComboBoxItem | string)[]
|
||||
) => {
|
||||
if (
|
||||
allowCustomValue ||
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === "" ||
|
||||
!items
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !items.some(
|
||||
(item) => typeof item !== "string" && item.id === value
|
||||
);
|
||||
private _setUnknownValue = () => {
|
||||
const items = this.getItems();
|
||||
if (
|
||||
this.allowCustomValue ||
|
||||
this.value === undefined ||
|
||||
this.value === null ||
|
||||
this.value === "" ||
|
||||
!items
|
||||
) {
|
||||
this._unknownValue = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._unknownValue = !items.some(
|
||||
(item) => typeof item !== "string" && item.id === this.value
|
||||
);
|
||||
};
|
||||
|
||||
private _throttleUnknownValue = throttle(
|
||||
this._setUnknownValue,
|
||||
1000,
|
||||
true,
|
||||
false
|
||||
);
|
||||
|
||||
private _renderHelper() {
|
||||
@@ -283,9 +312,16 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
</ha-input-helper-text>`;
|
||||
}
|
||||
|
||||
private _initialFieldValue?: string;
|
||||
|
||||
private _dialogOpened = () => {
|
||||
this._opened = true;
|
||||
requestAnimationFrame(() => {
|
||||
// Set initial field value if needed
|
||||
if (this._initialFieldValue) {
|
||||
this._comboBox?.setFieldValue(this._initialFieldValue);
|
||||
this._initialFieldValue = undefined;
|
||||
}
|
||||
if (this.hass && isIosApp(this.hass)) {
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "focus_element",
|
||||
@@ -295,6 +331,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._comboBox?.focus();
|
||||
});
|
||||
};
|
||||
@@ -376,6 +413,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
.container {
|
||||
position: relative;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
label[disabled] {
|
||||
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.6));
|
||||
|
||||
@@ -124,9 +124,6 @@ export class HaIconPicker extends LitElement {
|
||||
.label=${this.label}
|
||||
.value=${this._value}
|
||||
.searchFn=${this._filterIcons}
|
||||
.notFoundLabel=${this.hass?.localize(
|
||||
"ui.components.icon-picker.no_match"
|
||||
)}
|
||||
popover-placement="bottom-start"
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
@@ -173,20 +170,6 @@ export class HaIconPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Allow preview for custom icon not in list
|
||||
if (rankedItems.length === 0) {
|
||||
rankedItems.push({
|
||||
item: {
|
||||
id: filter,
|
||||
primary: filter,
|
||||
icon: filter,
|
||||
search_labels: { keyword: filter },
|
||||
sorting_label: filter,
|
||||
},
|
||||
rank: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return rankedItems
|
||||
.sort((itemA, itemB) => itemA.rank - itemB.rank)
|
||||
.map((item) => item.item);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiLabel, mdiPlus } from "@mdi/js";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
@@ -25,11 +25,9 @@ import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
const NO_LABELS = "___NO_LABELS___";
|
||||
|
||||
@customElement("ha-label-picker")
|
||||
export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -108,52 +106,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
private _labelMap = memoizeOne(
|
||||
(
|
||||
labels: LabelRegistryEntry[] | undefined
|
||||
): Map<string, LabelRegistryEntry> => {
|
||||
if (!labels) {
|
||||
return new Map();
|
||||
}
|
||||
return new Map(labels.map((label) => [label.label_id, label]));
|
||||
}
|
||||
);
|
||||
|
||||
private _computeValueRenderer = memoizeOne(
|
||||
(labels: LabelRegistryEntry[] | undefined): PickerValueRenderer =>
|
||||
(value) => {
|
||||
const label = this._labelMap(labels).get(value);
|
||||
|
||||
if (!label) {
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>
|
||||
<span slot="headline">${value}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${label.icon
|
||||
? html`<ha-icon slot="start" .icon=${label.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>`}
|
||||
<span slot="headline">${label.name}</span>
|
||||
`;
|
||||
}
|
||||
);
|
||||
|
||||
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||
|
||||
private _getItems = () => {
|
||||
if (!this._labels || this._labels.length === 0) {
|
||||
return [
|
||||
{
|
||||
id: NO_LABELS,
|
||||
primary: this.hass.localize("ui.components.label-picker.no_labels"),
|
||||
icon_path: mdiLabel,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return this._getLabelsMemoized(
|
||||
private _getItems = () =>
|
||||
this._getLabelsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
@@ -166,7 +122,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
this.entityFilter,
|
||||
this.excludeLabels
|
||||
);
|
||||
};
|
||||
|
||||
private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => {
|
||||
if (!labels) {
|
||||
@@ -219,8 +174,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.label-picker.label");
|
||||
|
||||
const valueRenderer = this._computeValueRenderer(this._labels);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.disabled=${this.disabled}
|
||||
@@ -237,7 +190,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.searchKeys=${labelComboBoxKeys}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
@@ -251,10 +203,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (value === NO_LABELS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
this._setValue(undefined);
|
||||
return;
|
||||
|
||||
@@ -138,10 +138,10 @@ export class HaMarkdown extends LitElement {
|
||||
--markdown-table-padding-inline: 0;
|
||||
--markdown-table-padding-block: 0;
|
||||
th {
|
||||
vertical-align: attr(align, center);
|
||||
vertical-align: attr(valign, middle);
|
||||
}
|
||||
td {
|
||||
vertical-align: attr(align, left);
|
||||
vertical-align: attr(valign, middle);
|
||||
}
|
||||
}
|
||||
table {
|
||||
|
||||
@@ -44,6 +44,7 @@ class HaNavigationList extends LitElement {
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${page.iconPath}
|
||||
.secondaryPath=${page.iconSecondaryPath}
|
||||
.viewBox=${page.iconViewBox}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import Fuse from "fuse.js";
|
||||
import { mdiDevices, mdiTextureBox } from "@mdi/js";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { titleCase } from "../common/string/title-case";
|
||||
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
|
||||
import { fetchConfig } from "../data/lovelace/config/types";
|
||||
import { getPanelIcon, getPanelTitle } from "../data/panel";
|
||||
import { findRelated, type RelatedResult } from "../data/search";
|
||||
import { PANEL_DASHBOARDS } from "../panels/config/lovelace/dashboards/ha-config-lovelace-dashboards";
|
||||
import { multiTermSortedSearch } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { ActionRelatedContext } from "../panels/lovelace/components/hui-action-editor";
|
||||
import "./ha-generic-picker";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-icon";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import {
|
||||
DEFAULT_SEARCH_KEYS,
|
||||
type PickerComboBoxItem,
|
||||
} from "./ha-picker-combo-box";
|
||||
|
||||
type NavigationGroup = "related" | "dashboards" | "views" | "other_routes";
|
||||
|
||||
const RELATED_SORT_PREFIX = {
|
||||
area: "0_area",
|
||||
device: "1_device",
|
||||
} as const;
|
||||
|
||||
interface NavigationItem extends PickerComboBoxItem {
|
||||
group: NavigationGroup;
|
||||
domain?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-navigation-picker")
|
||||
export class HaNavigationPicker extends LitElement {
|
||||
@@ -25,13 +50,57 @@ export class HaNavigationPicker extends LitElement {
|
||||
|
||||
@state() private _loading = true;
|
||||
|
||||
@property({ attribute: false }) public context?: ActionRelatedContext;
|
||||
|
||||
protected firstUpdated() {
|
||||
this._loadNavigationItems();
|
||||
}
|
||||
|
||||
private _navigationItems: PickerComboBoxItem[] = [];
|
||||
private _navigationItems: NavigationItem[] = [];
|
||||
|
||||
private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||
|
||||
private _navigationGroups: Record<NavigationGroup, NavigationItem[]> = {
|
||||
related: [],
|
||||
dashboards: [],
|
||||
views: [],
|
||||
other_routes: [],
|
||||
};
|
||||
|
||||
private _getRelatedItems = memoizeOne(
|
||||
async (_cacheKey: string, context: ActionRelatedContext) =>
|
||||
this._fetchRelatedItems(context),
|
||||
(newArgs, lastArgs) => newArgs[0] === lastArgs[0]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const sections = [
|
||||
...(this._navigationGroups.related.length
|
||||
? [
|
||||
{
|
||||
id: "related",
|
||||
label: this.hass.localize(
|
||||
"ui.components.navigation-picker.related"
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: "dashboards",
|
||||
label: this.hass.localize("ui.components.navigation-picker.dashboards"),
|
||||
},
|
||||
{
|
||||
id: "views",
|
||||
label: this.hass.localize("ui.components.navigation-picker.views"),
|
||||
},
|
||||
{
|
||||
id: "other_routes",
|
||||
label: this.hass.localize(
|
||||
"ui.components.navigation-picker.other_routes"
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
@@ -43,6 +112,8 @@ export class HaNavigationPicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.getItems=${this._getItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.sections=${sections}
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.navigation-picker.add_custom_path"
|
||||
)}
|
||||
@@ -55,9 +126,23 @@ export class HaNavigationPicker extends LitElement {
|
||||
private _valueRenderer = (itemId: string) => {
|
||||
const item = this._navigationItems.find((navItem) => navItem.id === itemId);
|
||||
return html`
|
||||
${item?.icon
|
||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${item?.domain
|
||||
? html`
|
||||
<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${item.domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
`
|
||||
: item?.icon
|
||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||
: item?.icon_path
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
<span slot="headline">${item?.primary || itemId}</span>
|
||||
${item?.primary
|
||||
? html`<span slot="supporting-text">${itemId}</span>`
|
||||
@@ -65,9 +150,106 @@ export class HaNavigationPicker extends LitElement {
|
||||
`;
|
||||
};
|
||||
|
||||
private _getItems = () => this._navigationItems;
|
||||
private _rowRenderer = (item: NavigationItem) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
${item.domain
|
||||
? html`
|
||||
<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${item.domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
`
|
||||
: item.icon
|
||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||
: item.icon_path
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
private _fuseIndexes = {
|
||||
related: memoizeOne((items: NavigationItem[]) =>
|
||||
Fuse.createIndex(DEFAULT_SEARCH_KEYS, items)
|
||||
),
|
||||
dashboards: memoizeOne((items: NavigationItem[]) =>
|
||||
Fuse.createIndex(DEFAULT_SEARCH_KEYS, items)
|
||||
),
|
||||
views: memoizeOne((items: NavigationItem[]) =>
|
||||
Fuse.createIndex(DEFAULT_SEARCH_KEYS, items)
|
||||
),
|
||||
other_routes: memoizeOne((items: NavigationItem[]) =>
|
||||
Fuse.createIndex(DEFAULT_SEARCH_KEYS, items)
|
||||
),
|
||||
};
|
||||
|
||||
private _getItems = (searchString?: string, section?: string) => {
|
||||
const getGroupItems = (group: NavigationGroup) => {
|
||||
let items = [...this._navigationGroups[group]].sort(
|
||||
this._sortBySortingLabel
|
||||
);
|
||||
|
||||
if (searchString) {
|
||||
const fuseIndex = this._fuseIndexes[group](items);
|
||||
items = multiTermSortedSearch(
|
||||
items,
|
||||
searchString,
|
||||
DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const items: (NavigationItem | string)[] = [];
|
||||
|
||||
const related = getGroupItems("related");
|
||||
const dashboards = getGroupItems("dashboards");
|
||||
const views = getGroupItems("views");
|
||||
const otherRoutes = getGroupItems("other_routes");
|
||||
|
||||
const addGroup = (group: NavigationGroup, groupItems: NavigationItem[]) => {
|
||||
if (section && section !== group) {
|
||||
return;
|
||||
}
|
||||
if (!section && groupItems.length) {
|
||||
items.push(
|
||||
this.hass.localize(`ui.components.navigation-picker.${group}`)
|
||||
);
|
||||
}
|
||||
items.push(...groupItems);
|
||||
};
|
||||
|
||||
addGroup("related", related);
|
||||
addGroup("dashboards", dashboards);
|
||||
addGroup("views", views);
|
||||
addGroup("other_routes", otherRoutes);
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
private _sortBySortingLabel = (
|
||||
itemA: PickerComboBoxItem,
|
||||
itemB: PickerComboBoxItem
|
||||
) =>
|
||||
caseInsensitiveStringCompare(
|
||||
itemA.sorting_label!,
|
||||
itemB.sorting_label!,
|
||||
this.hass.locale.language
|
||||
);
|
||||
|
||||
private async _loadNavigationItems() {
|
||||
await this._loadConfigEntries();
|
||||
const panels = Object.entries(this.hass!.panels).map(([id, panel]) => ({
|
||||
id,
|
||||
...panel,
|
||||
@@ -91,13 +273,19 @@ export class HaNavigationPicker extends LitElement {
|
||||
|
||||
const panelViewConfig = new Map(viewConfigs);
|
||||
|
||||
this._navigationItems = [];
|
||||
const related = this._navigationGroups.related;
|
||||
const dashboards: NavigationItem[] = [];
|
||||
const views: NavigationItem[] = [];
|
||||
const otherRoutes: NavigationItem[] = [];
|
||||
|
||||
for (const panel of panels) {
|
||||
const path = `/${panel.url_path}`;
|
||||
const panelTitle = getPanelTitle(this.hass, panel);
|
||||
const primary = panelTitle || path;
|
||||
this._navigationItems.push({
|
||||
const isDashboardPanel =
|
||||
panel.component_name === "lovelace" ||
|
||||
PANEL_DASHBOARDS.includes(panel.id);
|
||||
const panelItem: NavigationItem = {
|
||||
id: path,
|
||||
primary,
|
||||
secondary: panelTitle ? path : undefined,
|
||||
@@ -108,7 +296,14 @@ export class HaNavigationPicker extends LitElement {
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("_"),
|
||||
});
|
||||
group: isDashboardPanel ? "dashboards" : "other_routes",
|
||||
};
|
||||
|
||||
if (isDashboardPanel) {
|
||||
dashboards.push(panelItem);
|
||||
} else {
|
||||
otherRoutes.push(panelItem);
|
||||
}
|
||||
|
||||
const config = panelViewConfig.get(panel.id);
|
||||
|
||||
@@ -118,7 +313,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
const viewPath = `/${panel.url_path}/${view.path ?? index}`;
|
||||
const viewPrimary =
|
||||
view.title ?? (view.path ? titleCase(view.path) : `${index}`);
|
||||
this._navigationItems.push({
|
||||
views.push({
|
||||
id: viewPath,
|
||||
secondary: viewPath,
|
||||
icon: view.icon ?? "mdi:view-compact",
|
||||
@@ -127,13 +322,155 @@ export class HaNavigationPicker extends LitElement {
|
||||
viewPrimary.startsWith("/") ? `zzz${viewPrimary}` : viewPrimary,
|
||||
viewPath,
|
||||
].join("_"),
|
||||
group: "views",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this._navigationGroups = {
|
||||
related,
|
||||
dashboards,
|
||||
views,
|
||||
other_routes: otherRoutes,
|
||||
};
|
||||
|
||||
this._navigationItems = [
|
||||
...related,
|
||||
...dashboards,
|
||||
...views,
|
||||
...otherRoutes,
|
||||
];
|
||||
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("context")) {
|
||||
this._loadRelatedItems();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadRelatedItems() {
|
||||
const updateRelatedItems = (relatedItems: NavigationItem[]) => {
|
||||
this._navigationGroups = {
|
||||
...this._navigationGroups,
|
||||
related: relatedItems,
|
||||
};
|
||||
this._navigationItems = [
|
||||
...relatedItems,
|
||||
...this._navigationGroups.dashboards,
|
||||
...this._navigationGroups.views,
|
||||
...this._navigationGroups.other_routes,
|
||||
];
|
||||
};
|
||||
|
||||
if (!this.hass || (!this.context?.entity_id && !this.context?.area_id)) {
|
||||
updateRelatedItems([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const context = this.context;
|
||||
const contextMatches = () =>
|
||||
this.context?.entity_id === context?.entity_id &&
|
||||
this.context?.area_id === context?.area_id;
|
||||
|
||||
const items = await this._getRelatedItems(
|
||||
`${context.entity_id ?? ""}|${context.area_id ?? ""}`,
|
||||
context
|
||||
);
|
||||
if (contextMatches()) {
|
||||
updateRelatedItems(items);
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchRelatedItems(
|
||||
context: ActionRelatedContext
|
||||
): Promise<NavigationItem[]> {
|
||||
let relatedResult: RelatedResult | undefined;
|
||||
try {
|
||||
relatedResult = context.entity_id
|
||||
? await findRelated(this.hass, "entity", context.entity_id)
|
||||
: await findRelated(this.hass, "area", context.area_id!);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching related items for navigation picker", err);
|
||||
return [];
|
||||
}
|
||||
|
||||
const relatedDeviceIds = new Set(relatedResult?.device ?? []);
|
||||
const relatedAreaIds = new Set(relatedResult?.area ?? []);
|
||||
if (context.area_id) {
|
||||
relatedAreaIds.add(context.area_id);
|
||||
}
|
||||
|
||||
const createSortingLabel = (
|
||||
prefix: string,
|
||||
primary: string,
|
||||
path: string
|
||||
) =>
|
||||
[prefix, primary.startsWith("/") ? `zzz${primary}` : primary, path]
|
||||
.filter(Boolean)
|
||||
.join("_");
|
||||
|
||||
const relatedItems: NavigationItem[] = [];
|
||||
for (const deviceId of relatedDeviceIds) {
|
||||
const device = this.hass.devices[deviceId];
|
||||
const primary = device?.name_by_user ?? device?.name ?? deviceId;
|
||||
const path = `/config/devices/device/${deviceId}`;
|
||||
relatedItems.push({
|
||||
id: path,
|
||||
primary,
|
||||
secondary: path,
|
||||
icon: mdiDevices,
|
||||
sorting_label: createSortingLabel(
|
||||
RELATED_SORT_PREFIX.device,
|
||||
primary,
|
||||
path
|
||||
),
|
||||
group: "related",
|
||||
domain: device?.primary_config_entry
|
||||
? this._configEntryLookup[device.primary_config_entry]?.domain
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
for (const areaId of relatedAreaIds) {
|
||||
const area = this.hass.areas[areaId];
|
||||
const primary = area?.name ?? areaId;
|
||||
const path = `/config/areas/area/${areaId}`;
|
||||
relatedItems.push({
|
||||
id: path,
|
||||
primary,
|
||||
secondary: path,
|
||||
icon: area?.icon ?? mdiTextureBox,
|
||||
sorting_label: createSortingLabel(
|
||||
RELATED_SORT_PREFIX.area,
|
||||
primary,
|
||||
path
|
||||
),
|
||||
group: "related",
|
||||
});
|
||||
}
|
||||
|
||||
return relatedItems;
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
if (Object.keys(this._configEntryLookup).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
this._configEntryLookup = Object.fromEntries(
|
||||
configEntries.map((entry) => [entry.entry_id, entry])
|
||||
);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching config entries for navigation picker", err);
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
this._setValue(ev.detail.value);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
|
||||
import { mdiClose, mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
@@ -26,6 +26,8 @@ import "./chips/ha-chip-set";
|
||||
import "./chips/ha-filter-chip";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@@ -55,6 +57,7 @@ export interface PickerComboBoxItem {
|
||||
}
|
||||
|
||||
export const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
|
||||
const PADDING_ID = "___padding___";
|
||||
|
||||
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
|
||||
item
|
||||
@@ -108,7 +111,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
public getItems!: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
) => PickerComboBoxItem[] | undefined;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
@@ -146,21 +149,29 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
||||
|
||||
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
|
||||
@property({ type: Boolean, reflect: true }) public clearable = false;
|
||||
|
||||
@query("lit-virtualizer") public virtualizerElement?: LitVirtualizer;
|
||||
|
||||
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||
|
||||
@state() private _items: (PickerComboBoxItem | string)[] = [];
|
||||
@state() private _items: PickerComboBoxItem[] = [];
|
||||
|
||||
public setFieldValue(value: string) {
|
||||
if (this._searchFieldElement) {
|
||||
this._searchFieldElement.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this._virtualizerElement as HTMLElement | null;
|
||||
return this.virtualizerElement as HTMLElement | null;
|
||||
}
|
||||
|
||||
@state() private _sectionTitle?: string;
|
||||
|
||||
@state() private _valuePinned = true;
|
||||
|
||||
private _allItems: (PickerComboBoxItem | string)[] = [];
|
||||
private _allItems: PickerComboBoxItem[] = [];
|
||||
|
||||
private _selectedItemIndex = -1;
|
||||
|
||||
@@ -200,8 +211,17 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
return html`<ha-textfield
|
||||
.label=${searchLabel}
|
||||
@blur=${this._resetSelectedItem}
|
||||
@input=${this._filterChanged}
|
||||
></ha-textfield>
|
||||
.iconTrailing=${this.clearable && !!this._search}
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._clearSearch}
|
||||
slot="trailingIcon"
|
||||
.label=${this.hass?.localize("ui.common.clear") || "Clear"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</ha-textfield>
|
||||
${this._renderSectionButtons()}
|
||||
${this.sections?.length
|
||||
? html`
|
||||
@@ -237,6 +257,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
@unpinned=${this._handleUnpinned}
|
||||
@scroll=${this._onScrollList}
|
||||
@focus=${this._focusList}
|
||||
@blur=${this._resetSelectedItem}
|
||||
@visibilityChanged=${this._visibilityChanged}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
@@ -269,18 +290,18 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
@eventOptions({ passive: true })
|
||||
private _visibilityChanged(ev) {
|
||||
if (
|
||||
this._virtualizerElement &&
|
||||
this.virtualizerElement &&
|
||||
this.sectionTitleFunction &&
|
||||
this.sections?.length
|
||||
) {
|
||||
const firstItem = this._virtualizerElement.items[ev.first];
|
||||
const secondItem = this._virtualizerElement.items[ev.first + 1];
|
||||
const firstItem = this.virtualizerElement.items[ev.first];
|
||||
const secondItem = this.virtualizerElement.items[ev.first + 1];
|
||||
this._sectionTitle = this.sectionTitleFunction({
|
||||
firstIndex: ev.first,
|
||||
lastIndex: ev.last,
|
||||
firstItem: firstItem as PickerComboBoxItem | string,
|
||||
secondItem: secondItem as PickerComboBoxItem | string,
|
||||
itemsCount: this._virtualizerElement.items.length,
|
||||
firstItem: firstItem as PickerComboBoxItem,
|
||||
secondItem: secondItem as PickerComboBoxItem,
|
||||
itemsCount: this.virtualizerElement.items.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -294,7 +315,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this.getAdditionalItems?.(searchString) || [];
|
||||
|
||||
private _getItems = () => {
|
||||
let items = [...this.getItems(this._search, this.selectedSection)];
|
||||
let items = [...(this.getItems(this._search, this.selectedSection) || [])];
|
||||
|
||||
if (!this.sections?.length) {
|
||||
items = items.sort((entityA, entityB) => {
|
||||
@@ -323,28 +344,28 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
items.push(NO_ITEMS_AVAILABLE_ID);
|
||||
if (!items.length && !this.allowCustomValue) {
|
||||
items.push({ id: NO_ITEMS_AVAILABLE_ID, primary: "" });
|
||||
}
|
||||
|
||||
const additionalItems = this._getAdditionalItems();
|
||||
items.push(...additionalItems);
|
||||
|
||||
if (this.mode === "dialog") {
|
||||
items.push("padding"); // padding for safe area inset
|
||||
items.push({ id: PADDING_ID, primary: "" }); // padding for safe area inset
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
|
||||
private _renderItem = (item: PickerComboBoxItem, index: number) => {
|
||||
if (!item) {
|
||||
return nothing;
|
||||
}
|
||||
if (item === "padding") {
|
||||
if (item.id === PADDING_ID) {
|
||||
return html`<div class="bottom-padding"></div>`;
|
||||
}
|
||||
if (item === NO_ITEMS_AVAILABLE_ID) {
|
||||
if (item.id === NO_ITEMS_AVAILABLE_ID) {
|
||||
return html`
|
||||
<div class="combo-box-row">
|
||||
<ha-combo-box-item type="text" compact>
|
||||
@@ -396,9 +417,22 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _valueSelected = (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
const value = (ev.currentTarget as any).value as string;
|
||||
const index = Number((ev.currentTarget as any).index);
|
||||
const newValue = value?.trim();
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
this._fireSelectedEvents(newValue, index);
|
||||
};
|
||||
|
||||
private _fireSelectedEvents(value: string, index: number) {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "index-selected", { index });
|
||||
}
|
||||
|
||||
private _clearSearch = () => {
|
||||
if (this._searchFieldElement) {
|
||||
this._searchFieldElement.value = "";
|
||||
this._searchFieldElement.dispatchEvent(new Event("input"));
|
||||
}
|
||||
};
|
||||
|
||||
private _fuseIndex = memoizeOne(
|
||||
@@ -419,21 +453,18 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this._fuseIndex(
|
||||
this._allItems as PickerComboBoxItem[],
|
||||
this.searchKeys
|
||||
);
|
||||
const index = this._fuseIndex(this._allItems, this.searchKeys);
|
||||
|
||||
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
|
||||
this._allItems as PickerComboBoxItem[],
|
||||
this._allItems,
|
||||
searchString,
|
||||
this.searchKeys || DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
index
|
||||
) as (PickerComboBoxItem | string)[];
|
||||
);
|
||||
|
||||
if (!filteredItems.length) {
|
||||
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
||||
if (!filteredItems.length && !this.allowCustomValue) {
|
||||
filteredItems.push({ id: NO_ITEMS_AVAILABLE_ID, primary: "" });
|
||||
}
|
||||
|
||||
const additionalItems = this._getAdditionalItems(searchString);
|
||||
@@ -442,8 +473,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
if (this.searchFn) {
|
||||
filteredItems = this.searchFn(
|
||||
searchString,
|
||||
filteredItems as PickerComboBoxItem[],
|
||||
this._allItems as PickerComboBoxItem[]
|
||||
filteredItems,
|
||||
this._allItems
|
||||
);
|
||||
}
|
||||
|
||||
@@ -459,7 +490,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
this._items = filteredItems as PickerComboBoxItem[];
|
||||
this._items = filteredItems;
|
||||
}
|
||||
|
||||
this._selectedItemIndex = -1;
|
||||
@@ -483,8 +514,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this._items = this._getItems();
|
||||
|
||||
// Reset scroll position when filter changes
|
||||
if (this._virtualizerElement) {
|
||||
this._virtualizerElement.scrollToIndex(0);
|
||||
if (this.virtualizerElement) {
|
||||
this.virtualizerElement.scrollToIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,13 +538,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _selectNextItem = (ev?: KeyboardEvent) => {
|
||||
ev?.stopPropagation();
|
||||
ev?.preventDefault();
|
||||
if (!this._virtualizerElement) {
|
||||
if (!this.virtualizerElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._searchFieldElement?.focus();
|
||||
|
||||
const items = this._virtualizerElement.items as PickerComboBoxItem[];
|
||||
const items = this.virtualizerElement.items as PickerComboBoxItem[];
|
||||
|
||||
const maxItems = items.length - 1;
|
||||
|
||||
@@ -547,14 +578,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _selectPreviousItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if (!this._virtualizerElement) {
|
||||
if (!this.virtualizerElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._selectedItemIndex > 0) {
|
||||
const nextIndex = this._selectedItemIndex - 1;
|
||||
|
||||
const items = this._virtualizerElement.items as PickerComboBoxItem[];
|
||||
const items = this.virtualizerElement.items as PickerComboBoxItem[];
|
||||
|
||||
if (!items[nextIndex]) {
|
||||
return;
|
||||
@@ -576,13 +607,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _selectFirstItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
|
||||
if (!this.virtualizerElement || !this.virtualizerElement.items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = 0;
|
||||
|
||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||
if (typeof this.virtualizerElement.items[nextIndex] === "string") {
|
||||
this._selectedItemIndex = nextIndex + 1;
|
||||
} else {
|
||||
this._selectedItemIndex = nextIndex;
|
||||
@@ -593,13 +624,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _selectLastItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
|
||||
if (!this.virtualizerElement || !this.virtualizerElement.items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = this._virtualizerElement.items.length - 1;
|
||||
const nextIndex = this.virtualizerElement.items.length - 1;
|
||||
|
||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||
if (typeof this.virtualizerElement.items[nextIndex] === "string") {
|
||||
this._selectedItemIndex = nextIndex - 1;
|
||||
} else {
|
||||
this._selectedItemIndex = nextIndex;
|
||||
@@ -609,14 +640,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
};
|
||||
|
||||
private _scrollToSelectedItem = () => {
|
||||
this._virtualizerElement
|
||||
this.virtualizerElement
|
||||
?.querySelector(".selected")
|
||||
?.classList.remove("selected");
|
||||
|
||||
this._virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
|
||||
this.virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this._virtualizerElement
|
||||
this.virtualizerElement
|
||||
?.querySelector(`#list-item-${this._selectedItemIndex}`)
|
||||
?.classList.add("selected");
|
||||
});
|
||||
@@ -624,12 +655,20 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _pickSelectedItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
|
||||
|
||||
if (this._virtualizerElement?.items.length === 1) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: firstItem.id,
|
||||
if (
|
||||
this.virtualizerElement?.items?.length !== undefined &&
|
||||
this.virtualizerElement.items.length < 4 && // it still can have a section title and a padding item
|
||||
this.virtualizerElement.items.filter((item) => typeof item !== "string")
|
||||
.length === 1
|
||||
) {
|
||||
(
|
||||
this.virtualizerElement?.items as (PickerComboBoxItem | string)[]
|
||||
).forEach((item, index) => {
|
||||
if (typeof item !== "string") {
|
||||
this._fireSelectedEvents(item.id, index);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._selectedItemIndex === -1) {
|
||||
@@ -639,16 +678,16 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
// if filter button is focused
|
||||
ev.preventDefault();
|
||||
|
||||
const item = this._virtualizerElement?.items[
|
||||
const item = this.virtualizerElement?.items[
|
||||
this._selectedItemIndex
|
||||
] as PickerComboBoxItem;
|
||||
if (item) {
|
||||
fireEvent(this, "value-changed", { value: item.id });
|
||||
this._fireSelectedEvents(item.id, this._selectedItemIndex);
|
||||
}
|
||||
};
|
||||
|
||||
private _resetSelectedItem() {
|
||||
this._virtualizerElement
|
||||
this.virtualizerElement
|
||||
?.querySelector(".selected")
|
||||
?.classList.remove("selected");
|
||||
this._selectedItemIndex = -1;
|
||||
@@ -658,11 +697,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
typeof item === "string" ? item : item?.id;
|
||||
|
||||
private _getInitialSelectedIndex() {
|
||||
if (!this._virtualizerElement || this._search || !this.value) {
|
||||
if (!this.virtualizerElement || this._search || !this.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const index = this._virtualizerElement.items.findIndex(
|
||||
const index = this.virtualizerElement.items.findIndex(
|
||||
(item) =>
|
||||
typeof item !== "string" &&
|
||||
(item as PickerComboBoxItem).id === this.value
|
||||
@@ -687,6 +726,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:host([clearable]) {
|
||||
--text-field-padding: 0 0 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
padding: 0 var(--ha-space-3);
|
||||
margin-bottom: var(--ha-space-3);
|
||||
@@ -788,8 +831,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
.section-title,
|
||||
.title {
|
||||
box-sizing: border-box;
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
padding: var(--ha-space-1) var(--ha-space-4);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
min-height: var(--ha-space-6);
|
||||
@@ -818,7 +862,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
width: calc(100% - var(--ha-space-8));
|
||||
width: calc(100% - var(--ha-space-4));
|
||||
}
|
||||
|
||||
.section-title.show {
|
||||
@@ -842,4 +886,8 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-picker-combo-box": HaPickerComboBox;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"index-selected": { index: number };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +223,6 @@ export class HaRelatedItems extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain: entry.domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
@@ -249,7 +248,6 @@ export class HaRelatedItems extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain: integration,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
|
||||
@@ -10,7 +10,7 @@ class HaSectionTitle extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
padding: var(--ha-space-2) var(--ha-space-3);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
min-height: var(--ha-space-6);
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-tooltip";
|
||||
|
||||
export interface Segment {
|
||||
value: number;
|
||||
color: string;
|
||||
label?: TemplateResult | string;
|
||||
entityId?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-segmented-bar")
|
||||
@@ -24,20 +27,32 @@ class HaSegmentedBar extends LitElement {
|
||||
@property({ type: Boolean, attribute: "hide-tooltip" })
|
||||
public hideTooltip = false;
|
||||
|
||||
@property({ type: Boolean }) public clickable = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "bar-clickable" })
|
||||
public barClickable = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public hiddenSegments?: number[];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const totalValue = this.segments.reduce(
|
||||
(acc, segment) => acc + segment.value,
|
||||
0
|
||||
);
|
||||
const totalValue = this.segments.reduce((acc, segment, index) => {
|
||||
if (this.hiddenSegments?.includes(index)) return acc;
|
||||
return acc + segment.value;
|
||||
}, 0);
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="heading">
|
||||
<div class="title">
|
||||
<span>${this.heading}</span>
|
||||
<span>${this.description}</span>
|
||||
</div>
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
${this.heading || this.description
|
||||
? html`
|
||||
<div class="heading">
|
||||
<div class="title">
|
||||
<span>${this.heading}</span>
|
||||
<span>${this.description}</span>
|
||||
</div>
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="bar">
|
||||
${this.segments.map(
|
||||
(segment, index) => html`
|
||||
@@ -50,9 +65,15 @@ class HaSegmentedBar extends LitElement {
|
||||
`}
|
||||
<div
|
||||
id="segment-${index}"
|
||||
class=${classMap({ clickable: this.barClickable })}
|
||||
data-index=${index}
|
||||
@click=${this.barClickable ? this._handleSegmentClick : nothing}
|
||||
style=${styleMap({
|
||||
width: `${(segment.value / totalValue) * 100}%`,
|
||||
backgroundColor: segment.color,
|
||||
display: this.hiddenSegments?.includes(index)
|
||||
? "none"
|
||||
: "block",
|
||||
})}
|
||||
></div>
|
||||
`
|
||||
@@ -62,10 +83,19 @@ class HaSegmentedBar extends LitElement {
|
||||
? nothing
|
||||
: html`
|
||||
<ul class="legend">
|
||||
${this.segments.map((segment) =>
|
||||
${this.segments.map((segment, index) =>
|
||||
segment.label
|
||||
? html`
|
||||
<li>
|
||||
<li
|
||||
class=${classMap({
|
||||
clickable: this.clickable,
|
||||
hidden: this.hiddenSegments?.includes(index),
|
||||
})}
|
||||
data-index=${index}
|
||||
@click=${this.clickable
|
||||
? this._handleLegendClick
|
||||
: nothing}
|
||||
>
|
||||
<div
|
||||
class="bullet"
|
||||
style=${styleMap({
|
||||
@@ -83,6 +113,24 @@ class HaSegmentedBar extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSegmentClick(ev: Event): void {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
const index = Number(target.dataset.index);
|
||||
const segment = this.segments[index];
|
||||
if (segment) {
|
||||
fireEvent(this, "segment-clicked", { index, segment });
|
||||
}
|
||||
}
|
||||
|
||||
private _handleLegendClick(ev: Event): void {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
const index = Number(target.dataset.index);
|
||||
const segment = this.segments[index];
|
||||
if (segment) {
|
||||
fireEvent(this, "legend-item-clicked", { index, segment });
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
width: 100%;
|
||||
@@ -118,7 +166,10 @@ class HaSegmentedBar extends LitElement {
|
||||
.bar div {
|
||||
height: 100%;
|
||||
}
|
||||
.bar div:hover {
|
||||
.bar div.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.bar div.clickable:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.legend {
|
||||
@@ -144,6 +195,18 @@ class HaSegmentedBar extends LitElement {
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.legend li.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.legend li.clickable:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.legend li.hidden {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.legend li.hidden .label {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -151,4 +214,8 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-segmented-bar": HaSegmentedBar;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"segment-clicked": { index: number; segment: Segment };
|
||||
"legend-item-clicked": { index: number; segment: Segment };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,13 @@ export class HaChooseSelector extends LitElement {
|
||||
) {
|
||||
this._setActiveChoice();
|
||||
}
|
||||
if (
|
||||
changedProperties.has("value") &&
|
||||
changedProperties.get("value")?.active_choice &&
|
||||
changedProperties.get("value")?.active_choice !== this._activeChoice
|
||||
) {
|
||||
this._setActiveChoice();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -54,7 +61,8 @@ export class HaChooseSelector extends LitElement {
|
||||
size="small"
|
||||
.buttons=${this._toggleButtons(
|
||||
this.selector.choose.choices,
|
||||
this.selector.choose.translation_key
|
||||
this.selector.choose.translation_key,
|
||||
this.hass.localize
|
||||
)}
|
||||
.active=${this._activeChoice}
|
||||
@value-changed=${this._choiceChanged}
|
||||
@@ -72,7 +80,11 @@ export class HaChooseSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _toggleButtons = memoizeOne(
|
||||
(choices: ChooseSelector["choose"]["choices"], translationKey?: string) =>
|
||||
(
|
||||
choices: ChooseSelector["choose"]["choices"],
|
||||
translationKey?: string,
|
||||
_localize?: HomeAssistant["localize"]
|
||||
) =>
|
||||
Object.keys(choices).map((choice) => ({
|
||||
label:
|
||||
this.localizeValue && translationKey
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { DurationSelector } from "../../data/selector";
|
||||
@@ -11,7 +12,10 @@ export class HaTimeDuration extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public selector!: DurationSelector;
|
||||
|
||||
@property({ attribute: false }) public value?: HaDurationData;
|
||||
@property({ attribute: false }) public value?:
|
||||
| HaDurationData
|
||||
| string
|
||||
| number;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -21,16 +25,47 @@ export class HaTimeDuration extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
private _data = memoizeOne(
|
||||
(value?: HaDurationData | string | number): HaDurationData | undefined => {
|
||||
if (typeof value === "number") {
|
||||
return { seconds: value };
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const negative = value.trim()[0] === "-";
|
||||
const parts = value
|
||||
.split(":")
|
||||
.map((p) => (negative && p ? -Math.abs(Number(p)) : Number(p)));
|
||||
|
||||
if (parts.length === 1) {
|
||||
return { seconds: parts[0] };
|
||||
}
|
||||
if (parts.length === 2) {
|
||||
return { hours: parts[0], minutes: parts[1] };
|
||||
}
|
||||
if (parts.length === 3) {
|
||||
return {
|
||||
hours: parts[0],
|
||||
minutes: parts[1],
|
||||
seconds: parts[2],
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-duration-input
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.data=${this.value}
|
||||
.data=${this._data(this.value)}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.enableDay=${this.selector.duration?.enable_day}
|
||||
.enableMillisecond=${this.selector.duration?.enable_millisecond}
|
||||
.allowNegative=${this.selector.duration?.allow_negative}
|
||||
></ha-duration-input>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@ export class HaMediaSelector extends LitElement {
|
||||
this._thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnail),
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { NavigationSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ActionRelatedContext } from "../../panels/lovelace/components/hui-action-editor";
|
||||
import "../ha-navigation-picker";
|
||||
|
||||
@customElement("ha-selector-navigation")
|
||||
@@ -21,6 +22,8 @@ export class HaNavigationSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ attribute: false }) public context?: ActionRelatedContext;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-navigation-picker
|
||||
@@ -30,6 +33,7 @@ export class HaNavigationSelector extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
.context=${this.selector.navigation ?? this.context}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-navigation-picker>
|
||||
`;
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { StateSelector } from "../../data/selector";
|
||||
import { extractFromTarget } from "../../data/target";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
resolveEntityIDs,
|
||||
type StateSelector,
|
||||
type TargetSelector,
|
||||
} from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-state-picker";
|
||||
import "../entity/ha-entity-states-picker";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
|
||||
@customElement("ha-selector-state")
|
||||
export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
@@ -28,16 +33,33 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
filter_attribute?: string;
|
||||
filter_entity?: string | string[];
|
||||
filter_target?: HassServiceTarget;
|
||||
target_selector?: TargetSelector;
|
||||
};
|
||||
|
||||
@state() private _entityIds?: string | string[];
|
||||
|
||||
private _convertExtraOptions = memoizeOne(
|
||||
(
|
||||
extraOptions?: { label: string; value: any }[]
|
||||
): PickerComboBoxItem[] | undefined => {
|
||||
if (!extraOptions) {
|
||||
return undefined;
|
||||
}
|
||||
return extraOptions.map((option) => ({
|
||||
id: option.value,
|
||||
primary: option.label,
|
||||
sorting_label: option.label,
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
willUpdate(changedProps) {
|
||||
if (changedProps.has("selector") || changedProps.has("context")) {
|
||||
this._resolveEntityIds(
|
||||
this.selector.state?.entity_id,
|
||||
this.context?.filter_entity,
|
||||
this.context?.filter_target
|
||||
this.context?.filter_target,
|
||||
this.context?.target_selector
|
||||
).then((entityIds) => {
|
||||
this._entityIds = entityIds;
|
||||
});
|
||||
@@ -45,6 +67,9 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const extraOptions = this._convertExtraOptions(
|
||||
this.selector.state?.extra_options
|
||||
);
|
||||
if (this.selector.state?.multiple) {
|
||||
return html`
|
||||
<ha-entity-states-picker
|
||||
@@ -52,7 +77,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
.entityId=${this._entityIds}
|
||||
.attribute=${this.selector.state?.attribute ||
|
||||
this.context?.filter_attribute}
|
||||
.extraOptions=${this.selector.state?.extra_options}
|
||||
.extraOptions=${extraOptions}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
@@ -69,7 +94,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
.entityId=${this._entityIds}
|
||||
.attribute=${this.selector.state?.attribute ||
|
||||
this.context?.filter_attribute}
|
||||
.extraOptions=${this.selector.state?.extra_options}
|
||||
.extraOptions=${extraOptions}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
@@ -84,7 +109,8 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
private async _resolveEntityIds(
|
||||
selectorEntityId: string | string[] | undefined,
|
||||
contextFilterEntity: string | string[] | undefined,
|
||||
contextFilterTarget: HassServiceTarget | undefined
|
||||
contextFilterTarget: HassServiceTarget | undefined,
|
||||
contextTargetSelector: TargetSelector | undefined
|
||||
): Promise<string | string[] | undefined> {
|
||||
if (selectorEntityId !== undefined) {
|
||||
return selectorEntityId;
|
||||
@@ -93,8 +119,14 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
return contextFilterEntity;
|
||||
}
|
||||
if (contextFilterTarget !== undefined) {
|
||||
const result = await extractFromTarget(this.hass, contextFilterTarget);
|
||||
return result.referenced_entities;
|
||||
return resolveEntityIDs(
|
||||
this.hass,
|
||||
contextFilterTarget,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
contextTargetSelector
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { ActionConfig } from "../../data/lovelace/config/action";
|
||||
import type { UiActionSelector } from "../../data/selector";
|
||||
import "../../panels/lovelace/components/hui-action-editor";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ActionRelatedContext } from "../../panels/lovelace/components/hui-action-editor";
|
||||
|
||||
@customElement("ha-selector-ui_action")
|
||||
export class HaSelectorUiAction extends LitElement {
|
||||
@@ -14,6 +15,8 @@ export class HaSelectorUiAction extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public value?: ActionConfig;
|
||||
|
||||
@property({ attribute: false }) public context?: ActionRelatedContext;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
@@ -24,6 +27,7 @@ export class HaSelectorUiAction extends LitElement {
|
||||
.label=${this.label}
|
||||
.hass=${this.hass}
|
||||
.config=${this.value}
|
||||
.context=${this.context}
|
||||
.actions=${this.selector.ui_action?.actions}
|
||||
.defaultAction=${this.selector.ui_action?.default_action}
|
||||
.tooltipText=${this.helper}
|
||||
|
||||
@@ -37,6 +37,7 @@ export class HaSelectorUiStateContent extends SubscribeMixin(LitElement) {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.allowName=${this.selector.ui_state_content?.allow_name || false}
|
||||
.allowContext=${this.selector.ui_state_content?.allow_context || false}
|
||||
></ha-entity-state-content-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -52,8 +52,6 @@ import "./ha-spinner";
|
||||
import "./ha-svg-icon";
|
||||
import "./user/ha-user-badge";
|
||||
|
||||
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||
|
||||
const SORT_VALUE_URL_PATHS = {
|
||||
energy: 1,
|
||||
map: 2,
|
||||
@@ -344,17 +342,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
this._calculateCounts();
|
||||
|
||||
if (!SUPPORT_SCROLL_IF_NEEDED) {
|
||||
return;
|
||||
}
|
||||
if (oldHass?.panelUrl !== this.hass.panelUrl) {
|
||||
const selectedEl = this.shadowRoot!.querySelector(".selected");
|
||||
if (selectedEl) {
|
||||
// @ts-ignore
|
||||
selectedEl.scrollIntoViewIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _calculateCounts = throttle(() => {
|
||||
@@ -602,10 +589,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
// On keypresses on the listbox, we're going to ignore mouse enter events
|
||||
// for 100ms so that we ignore it when pressing down arrow scrolls the
|
||||
// sidebar causing the mouse to hover a new icon
|
||||
if (
|
||||
this.alwaysExpand ||
|
||||
new Date().getTime() < this._recentKeydownActiveUntil
|
||||
) {
|
||||
if (new Date().getTime() < this._recentKeydownActiveUntil) {
|
||||
return;
|
||||
}
|
||||
if (this._mouseLeaveTimeout) {
|
||||
@@ -625,7 +609,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _listboxFocusIn(ev) {
|
||||
if (this.alwaysExpand || ev.target.localName !== "ha-md-list-item") {
|
||||
if (ev.target.localName !== "ha-md-list-item") {
|
||||
return;
|
||||
}
|
||||
this._showTooltip(ev.target);
|
||||
@@ -665,6 +649,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
clearTimeout(this._tooltipHideTimeout);
|
||||
this._tooltipHideTimeout = undefined;
|
||||
}
|
||||
const itemText = item.querySelector(".item-text") as HTMLElement | null;
|
||||
if (this.hasAttribute("expanded") && itemText) {
|
||||
const isTruncated = itemText.scrollWidth > itemText.clientWidth;
|
||||
if (!isTruncated) {
|
||||
this._hideTooltip();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const tooltip = this._tooltip;
|
||||
const allListbox = this.shadowRoot!.querySelectorAll("ha-md-list")!;
|
||||
const listbox = [...allListbox].find((lb) => lb.contains(item));
|
||||
@@ -675,9 +667,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
(listbox?.offsetTop ?? 0) -
|
||||
(listbox?.scrollTop ?? 0);
|
||||
|
||||
tooltip.innerText = (
|
||||
item.querySelector(".item-text") as HTMLElement
|
||||
).innerText;
|
||||
tooltip.innerText = itemText?.innerText ?? "";
|
||||
tooltip.style.display = "block";
|
||||
tooltip.style.position = "fixed";
|
||||
tooltip.style.top = `${top}px`;
|
||||
@@ -859,6 +849,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
:host([expanded]) ha-md-list-item .item-text {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.divider {
|
||||
@@ -926,7 +919,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
position: absolute;
|
||||
opacity: 0.9;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
white-space: nowrap;
|
||||
max-width: calc(var(--ha-space-20) * 3);
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
color: var(--sidebar-background-color);
|
||||
background-color: var(--sidebar-text-color);
|
||||
padding: var(--ha-space-1);
|
||||
|
||||
@@ -57,6 +57,7 @@ export class HaSlider extends Slider {
|
||||
#thumb {
|
||||
border: none;
|
||||
background-color: var(--ha-slider-thumb-color, var(--primary-color));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#thumb:after {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import type WaDialog from "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -13,7 +14,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@@ -50,7 +50,6 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
* @cssprop --ha-dialog-hide-duration - Hide animation duration.
|
||||
* @cssprop --ha-dialog-surface-background - Dialog background color.
|
||||
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
|
||||
* @cssprop --dialog-z-index - Z-index for the dialog.
|
||||
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
|
||||
*
|
||||
* @attr {boolean} open - Controls the dialog open state.
|
||||
@@ -107,6 +106,9 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||
public flexContent = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "without-header" })
|
||||
public withoutHeader = false;
|
||||
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
@@ -115,6 +117,8 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@state()
|
||||
private _bodyScrolled = false;
|
||||
|
||||
private _escapePressed = false;
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this.bodyContainer;
|
||||
}
|
||||
@@ -140,33 +144,41 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
(this.headerTitle !== undefined ? "ha-wa-dialog-title" : undefined)
|
||||
)}
|
||||
aria-describedby=${ifDefined(this.ariaDescribedBy)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@wa-hide=${this._handleHide}
|
||||
@wa-show=${this._handleShow}
|
||||
@wa-after-show=${this._handleAfterShow}
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
>
|
||||
<slot name="header">
|
||||
<ha-dialog-header
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
.showBorder=${this._bodyScrolled}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>
|
||||
${!this.withoutHeader
|
||||
? html` <slot name="header">
|
||||
<ha-dialog-header
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
.showBorder=${this._bodyScrolled}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span
|
||||
slot="title"
|
||||
class="title"
|
||||
id="ha-wa-dialog-title"
|
||||
>
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>`
|
||||
: nothing}
|
||||
<div class="content-wrapper">
|
||||
<div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
|
||||
<slot></slot>
|
||||
@@ -185,21 +197,22 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (isIosApp(this.hass)) {
|
||||
const element = this.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-wa-dialog-autofocus";
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
// temporary disabled because of issues with focus in iOS app, can be reenabled in 2026.2.0
|
||||
// if (isIosApp(this.hass)) {
|
||||
// const element = this.querySelector("[autofocus]");
|
||||
// if (element !== null) {
|
||||
// if (!element.id) {
|
||||
// element.id = "ha-wa-dialog-autofocus";
|
||||
// }
|
||||
// this.hass.auth.external!.fireMessage({
|
||||
// type: "focus_element",
|
||||
// payload: {
|
||||
// element_id: element.id,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
|
||||
});
|
||||
};
|
||||
@@ -208,9 +221,11 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
fireEvent(this, "after-show");
|
||||
};
|
||||
|
||||
private _handleAfterHide = () => {
|
||||
this._open = false;
|
||||
fireEvent(this, "closed");
|
||||
private _handleAfterHide = (ev: CustomEvent<{ source: Element }>) => {
|
||||
if (ev.eventPhase === Event.AT_TARGET) {
|
||||
this._open = false;
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
@@ -223,6 +238,23 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
|
||||
}
|
||||
|
||||
private _handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Escape") {
|
||||
this._escapePressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleHide(ev: CustomEvent<{ source: Element }>) {
|
||||
if (
|
||||
this.preventScrimClose &&
|
||||
this._escapePressed &&
|
||||
ev.detail.source === (ev.target as WaDialog).dialog
|
||||
) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
this._escapePressed = false;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
@@ -271,6 +303,7 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
color: var(--primary-text-color);
|
||||
min-width: var(--width, var(--full-width));
|
||||
max-width: var(--width, var(--full-width));
|
||||
max-height: var(
|
||||
|
||||
@@ -24,6 +24,7 @@ import { setupLeafletMap } from "../../common/dom/setup-leaflet-map";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { DecoratedMarker } from "../../common/map/decorated_marker";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import { isTouch } from "../../util/is_touch";
|
||||
import "../ha-icon-button";
|
||||
@@ -381,7 +382,7 @@ export class HaMap extends ReactiveElement {
|
||||
this.hass.config
|
||||
);
|
||||
}
|
||||
return `${path.name}<br>${formattedTime}`;
|
||||
return `${filterXSS(path.name ?? "")}<br>${formattedTime}`;
|
||||
}
|
||||
|
||||
private _drawPaths(): void {
|
||||
@@ -549,7 +550,7 @@ export class HaMap extends ReactiveElement {
|
||||
iconHTML = el.outerHTML;
|
||||
} else {
|
||||
const el = document.createElement("span");
|
||||
el.innerHTML = title;
|
||||
el.textContent = title;
|
||||
iconHTML = el.outerHTML;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiAlphaABoxOutline,
|
||||
mdiArrowLeft,
|
||||
mdiClose,
|
||||
mdiDotsVertical,
|
||||
mdiGrid,
|
||||
@@ -21,9 +19,12 @@ import type {
|
||||
} from "../../data/media-player";
|
||||
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-dialog";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-list-item";
|
||||
import "../ha-dropdown";
|
||||
import "../ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../ha-dropdown-item";
|
||||
import "../ha-icon-button-arrow-prev";
|
||||
import "../ha-wa-dialog";
|
||||
import "./ha-media-manage-button";
|
||||
import "./ha-media-player-browse";
|
||||
import type {
|
||||
@@ -44,6 +45,8 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
|
||||
@state() _preferredLayout: MediaPlayerLayoutType = "auto";
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;
|
||||
|
||||
public showDialog(params: MediaPlayerBrowseDialogParams): void {
|
||||
@@ -54,9 +57,11 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
media_content_type: undefined,
|
||||
},
|
||||
];
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
this._params = undefined;
|
||||
this._navigateIds = undefined;
|
||||
this._currentItem = undefined;
|
||||
@@ -71,28 +76,20 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
flexContent
|
||||
.heading=${!this._currentItem
|
||||
? this.hass.localize(
|
||||
"ui.components.media-browser.media-player-browser"
|
||||
)
|
||||
: this._currentItem.title}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
flexcontent
|
||||
@closed=${this.closeDialog}
|
||||
@opened=${this._dialogOpened}
|
||||
>
|
||||
<ha-dialog-header show-border slot="heading">
|
||||
<ha-dialog-header show-border slot="header">
|
||||
${this._navigateIds.length > (this._params.minimumNavigateLevel ?? 1)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
<ha-icon-button-arrow-prev
|
||||
slot="navigationIcon"
|
||||
.path=${mdiArrowLeft}
|
||||
@click=${this._goBack}
|
||||
></ha-icon-button>
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: nothing}
|
||||
<span slot="title">
|
||||
@@ -108,52 +105,51 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
.currentItem=${this._currentItem}
|
||||
@media-refresh=${this._refreshMedia}
|
||||
></ha-media-manage-button>
|
||||
<ha-button-menu
|
||||
<ha-dropdown
|
||||
slot="actionItems"
|
||||
@action=${this._handleMenuAction}
|
||||
@wa-select=${this._handleMenuAction}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize("ui.components.media-browser.auto")}
|
||||
<ha-dropdown-item value="auto">
|
||||
<ha-svg-icon
|
||||
class=${this._preferredLayout === "auto"
|
||||
? "selected_menu_item"
|
||||
: ""}
|
||||
slot="graphic"
|
||||
slot="icon"
|
||||
.path=${mdiAlphaABoxOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize("ui.components.media-browser.grid")}
|
||||
${this.hass.localize("ui.components.media-browser.auto")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="grid">
|
||||
<ha-svg-icon
|
||||
class=${this._preferredLayout === "grid"
|
||||
? "selected_menu_item"
|
||||
: ""}
|
||||
slot="graphic"
|
||||
slot="icon"
|
||||
.path=${mdiGrid}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize("ui.components.media-browser.list")}
|
||||
${this.hass.localize("ui.components.media-browser.grid")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="list">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="icon"
|
||||
class=${this._preferredLayout === "list"
|
||||
? "selected_menu_item"
|
||||
: ""}
|
||||
.path=${mdiListBoxOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
${this.hass.localize("ui.components.media-browser.list")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
data-dialog="close"
|
||||
slot="actionItems"
|
||||
></ha-icon-button>
|
||||
</ha-dialog-header>
|
||||
@@ -173,7 +169,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
@media-picked=${this._mediaPicked}
|
||||
@media-browsed=${this._mediaBrowsed}
|
||||
></ha-media-player-browse>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -181,15 +177,16 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
this.classList.add("opened");
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
private async _handleMenuAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const action = ev.detail?.item?.value;
|
||||
switch (action) {
|
||||
case "auto":
|
||||
this._preferredLayout = "auto";
|
||||
break;
|
||||
case 1:
|
||||
case "grid":
|
||||
this._preferredLayout = "grid";
|
||||
break;
|
||||
case 2:
|
||||
case "list":
|
||||
this._preferredLayout = "list";
|
||||
break;
|
||||
}
|
||||
@@ -225,8 +222,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 9;
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
@@ -241,9 +237,9 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
--mdc-dialog-max-height: calc(
|
||||
ha-wa-dialog {
|
||||
--ha-dialog-max-width: 800px;
|
||||
--ha-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -793,7 +793,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { type CSSResultGroup, LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { mdiSpeaker } from "@mdi/js";
|
||||
import { mdiSpeaker, mdiSpeakerPause, mdiSpeakerPlay } from "@mdi/js";
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
import "../ha-switch";
|
||||
import "../ha-svg-icon";
|
||||
import type { MediaPlayerEntity } from "../../data/media-player";
|
||||
|
||||
@customElement("ha-media-player-toggle")
|
||||
class HaMediaPlayerToggle extends LitElement {
|
||||
@@ -20,15 +21,61 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
private _computeDisplayData = memoizeOne(
|
||||
(
|
||||
entityId: string,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"],
|
||||
isRTL: boolean,
|
||||
stateObj: HomeAssistant["states"][string]
|
||||
) => {
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
return { primary, secondary };
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const stateObj = this.hass.states[this.entityId];
|
||||
|
||||
let icon = mdiSpeaker;
|
||||
if (stateObj.state === "playing") {
|
||||
icon = mdiSpeakerPlay;
|
||||
} else if (stateObj.state === "paused") {
|
||||
icon = mdiSpeakerPause;
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
const { primary, secondary } = this._computeDisplayData(
|
||||
this.entityId,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors,
|
||||
isRTL,
|
||||
stateObj
|
||||
);
|
||||
|
||||
return html`<div class="list-item">
|
||||
<ha-svg-icon .path=${mdiSpeaker}></ha-svg-icon>
|
||||
<ha-svg-icon .path=${icon}></ha-svg-icon>
|
||||
<div class="info">
|
||||
<div class="main-text">${computeStateName(stateObj)}</div>
|
||||
<div class="secondary-text">
|
||||
${this._formatSecondaryText(stateObj as MediaPlayerEntity)}
|
||||
</div>
|
||||
<div class="main-text">${primary}</div>
|
||||
<div class="secondary-text">${secondary}</div>
|
||||
</div>
|
||||
<ha-switch
|
||||
.disabled=${this.disabled}
|
||||
@@ -38,16 +85,6 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _formatSecondaryText(stateObj: MediaPlayerEntity): string {
|
||||
if (stateObj.state !== "playing") {
|
||||
return this.hass.localize("ui.card.media_player.idle");
|
||||
}
|
||||
|
||||
return [stateObj.attributes.media_title, stateObj.attributes.media_artist]
|
||||
.filter((segment) => segment)
|
||||
.join(" · ");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user