mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-30 01:24:19 +00:00
Compare commits
20
Commits
rc
...
actions-lockfile
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
382fca108b | ||
|
|
1c50ae4f0b | ||
|
|
b79c6350ec | ||
|
|
8148041ddc | ||
|
|
4265624c0e | ||
|
|
ed4179de8f | ||
|
|
e407a1b2dd | ||
|
|
e851101a93 | ||
|
|
d43e1ef4cc | ||
|
|
f699019994 | ||
|
|
fe90b0339d | ||
|
|
3b5ba802f6 | ||
|
|
b0c6e7f564 | ||
|
|
658a23724e | ||
|
|
f859183628 | ||
|
|
53dc14f413 | ||
|
|
f2523095ff | ||
|
|
ef5c04b67f | ||
|
|
10ed6f709e | ||
|
|
c22de3bc7c |
No files matched your search
@@ -12,3 +12,7 @@
|
||||
|
||||
demo/public/api/camera_proxy_stream/* binary
|
||||
demo/public/api/media_player_proxy/* binary
|
||||
|
||||
# Generated by `gh actions-lock`. Highlight it as YAML and don't auto-merge it:
|
||||
# on a conflict, restore it from the base branch and run `gh actions-lock`.
|
||||
.github/workflows/actions.lock linguist-language=YAML -merge
|
||||
@@ -0,0 +1,6 @@
|
||||
paths:
|
||||
.github/workflows/**/*.{yml,yaml}:
|
||||
ignore:
|
||||
# actionlint doesn't support `$/` self-repository actions yet, remove once it does:
|
||||
# https://github.com/rhysd/actionlint/issues/711
|
||||
- 'specifying action "\$/[^"]+" in invalid format because ref is missing'
|
||||
@@ -24,7 +24,7 @@ runs:
|
||||
|
||||
- name: Setup Node and install
|
||||
if: steps.dependencies.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
cache: false
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Verify actions lockfile
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
paths:
|
||||
- ".github/actions/**"
|
||||
- ".github/workflows/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Verify actions lockfile
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_ACTIONS_LOCK_VERSION: v0.4.0
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install gh-actions-lock
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh extension install github/gh-actions-lock --pin "${GH_ACTIONS_LOCK_VERSION}"
|
||||
- name: Verify lockfile
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh actions-lock --verify
|
||||
@@ -0,0 +1,190 @@
|
||||
# This file is machine-generated by `gh actions-lock`.
|
||||
# Do not edit by hand; run `gh actions-lock` to update.
|
||||
# Docs: https://gh.io/actions-lockfile
|
||||
version: 'v0.0.3'
|
||||
workflows:
|
||||
'.github/workflows/actionlint.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
'.github/workflows/actions-lock.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
'.github/workflows/cast_deployment.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
'.github/workflows/ci.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
- 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9'
|
||||
'.github/workflows/codeql-analysis.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'github/codeql-action@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee'
|
||||
'.github/workflows/copilot-setup-steps.yml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
'.github/workflows/demo_deployment.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
'.github/workflows/dependency-review.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294'
|
||||
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
|
||||
'.github/workflows/design_deployment.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
'.github/workflows/design_preview.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
'.github/workflows/e2e.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333'
|
||||
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
- 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9'
|
||||
'.github/workflows/labeler.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
|
||||
- 'actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13'
|
||||
'.github/workflows/lock.yaml':
|
||||
- 'dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c'
|
||||
'.github/workflows/nightly.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
- 'actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97'
|
||||
- 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9'
|
||||
'.github/workflows/pull-request-standards.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
|
||||
'.github/workflows/relative-ci.yaml':
|
||||
- 'relative-ci/agent-action@711f3a8fa1b928e6afde280011110c9b0a224a60'
|
||||
'.github/workflows/release-drafter.yaml':
|
||||
- 'release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0'
|
||||
'.github/workflows/release.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
- 'actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97'
|
||||
- 'home-assistant/actions@cd6f3635a740c84d8a14f6121a90068b8ece6bf6'
|
||||
- 'home-assistant/wheels@9e17ab1ed5c4c79d8b61e29fa63de25ca2710716'
|
||||
- 'pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33'
|
||||
'.github/workflows/restrict-task-creation.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
|
||||
'.github/workflows/stale.yaml':
|
||||
- 'actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93'
|
||||
'.github/workflows/sync-device-classes.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
- 'peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1'
|
||||
'.github/workflows/sync-sensor-entity-constants.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
- 'peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1'
|
||||
'.github/workflows/translations.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
dependencies:
|
||||
'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9':
|
||||
ref: 'v6.1.0'
|
||||
commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
owner_id: 44036562
|
||||
repo_id: 215566462
|
||||
'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1':
|
||||
ref: 'v7.0.1'
|
||||
commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
owner_id: 44036562
|
||||
repo_id: 197814629
|
||||
'actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294':
|
||||
ref: 'v5.0.0'
|
||||
commit: 'sha1-a1d282b36b6f3519aa1f3fc636f609c47dddb294'
|
||||
owner_id: 44036562
|
||||
repo_id: 476372928
|
||||
'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333':
|
||||
ref: 'v8.0.2'
|
||||
commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333'
|
||||
owner_id: 44036562
|
||||
repo_id: 192626254
|
||||
'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3':
|
||||
ref: 'v9.0.0'
|
||||
commit: 'sha1-3a2844b7e9c422d3c10d287c895573f7108da1b3'
|
||||
owner_id: 44036562
|
||||
repo_id: 205262760
|
||||
'actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13':
|
||||
ref: 'v7.0.0'
|
||||
commit: 'sha1-bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13'
|
||||
owner_id: 44036562
|
||||
repo_id: 177139928
|
||||
'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1':
|
||||
ref: 'v7.1.0'
|
||||
commit: 'sha1-949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
owner_id: 44036562
|
||||
repo_id: 189476904
|
||||
'actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97':
|
||||
ref: 'v7.0.0'
|
||||
commit: 'sha1-5fda3b95a4ea91299a34e894583c3862153e4b97'
|
||||
owner_id: 44036562
|
||||
repo_id: 192625525
|
||||
'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405':
|
||||
ref: 'v6.2.0'
|
||||
commit: 'sha1-a309ff8b426b58ec0e2a45f0f869d46889d02405'
|
||||
owner_id: 44036562
|
||||
repo_id: 192625525
|
||||
'actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93':
|
||||
ref: 'v11.0.0'
|
||||
commit: 'sha1-4391f3da665fdf50b6810c1a66712fb9ba21aa93'
|
||||
owner_id: 44036562
|
||||
repo_id: 200437066
|
||||
'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9':
|
||||
ref: 'v7.0.2'
|
||||
commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9'
|
||||
owner_id: 44036562
|
||||
repo_id: 192625955
|
||||
'dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c':
|
||||
ref: 'v6.0.2'
|
||||
commit: 'sha1-89ae32b08ed1a541efecbab17912962a5e38981c'
|
||||
owner_id: 7513068
|
||||
repo_id: 113918130
|
||||
'github/codeql-action@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee':
|
||||
ref: 'v4.39.0'
|
||||
commit: 'sha1-6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee'
|
||||
owner_id: 9919
|
||||
repo_id: 259445878
|
||||
'home-assistant/actions@cd6f3635a740c84d8a14f6121a90068b8ece6bf6':
|
||||
ref: 'master'
|
||||
commit: 'sha1-cd6f3635a740c84d8a14f6121a90068b8ece6bf6'
|
||||
owner_id: 13844975
|
||||
repo_id: 173703505
|
||||
'home-assistant/wheels@9e17ab1ed5c4c79d8b61e29fa63de25ca2710716':
|
||||
ref: '2026.07.0'
|
||||
commit: 'sha1-9e17ab1ed5c4c79d8b61e29fa63de25ca2710716'
|
||||
owner_id: 13844975
|
||||
repo_id: 184234812
|
||||
'peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1':
|
||||
ref: 'v8.1.1'
|
||||
commit: 'sha1-5f6978faf089d4d20b00c7766989d076bb2fc7f1'
|
||||
owner_id: 18365890
|
||||
repo_id: 197176365
|
||||
'pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33':
|
||||
ref: 'v1.14.2'
|
||||
commit: 'sha1-dc37677b2e1c63e2034f94d8a5b11f265b73ba33'
|
||||
owner_id: 647025
|
||||
repo_id: 178055147
|
||||
uses:
|
||||
- 'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405'
|
||||
'relative-ci/agent-action@711f3a8fa1b928e6afde280011110c9b0a224a60':
|
||||
ref: 'v3.2.4'
|
||||
commit: 'sha1-711f3a8fa1b928e6afde280011110c9b0a224a60'
|
||||
owner_id: 43522883
|
||||
repo_id: 398376354
|
||||
'release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0':
|
||||
ref: 'v7.10.0'
|
||||
commit: 'sha1-978bb0b30c5ee8cdd332ced253758157150d43f0'
|
||||
owner_id: 58292376
|
||||
repo_id: 139022359
|
||||
@@ -30,17 +30,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
|
||||
- name: Build Cast
|
||||
uses: ./.github/actions/build
|
||||
uses: $/.github/actions/build
|
||||
with:
|
||||
target: build-cast
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: ./.github/actions/netlify-deploy
|
||||
uses: $/.github/actions/netlify-deploy
|
||||
with:
|
||||
dir: cast/dist
|
||||
alias: dev
|
||||
@@ -62,17 +62,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
|
||||
- name: Build Cast
|
||||
uses: ./.github/actions/build
|
||||
uses: $/.github/actions/build
|
||||
with:
|
||||
target: build-cast
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: ./.github/actions/netlify-deploy
|
||||
uses: $/.github/actions/netlify-deploy
|
||||
with:
|
||||
dir: cast/dist
|
||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Prepare dependencies
|
||||
uses: ./.github/actions/prepare-dependencies
|
||||
uses: $/.github/actions/prepare-dependencies
|
||||
|
||||
lint:
|
||||
name: Lint and check format
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
- name: Check for duplicate dependencies
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
- name: Build resources
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
# Read-only reuse of the rspack cache written by the nightly (see
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
restore-keys: |
|
||||
rspack-cache-${{ runner.os }}-
|
||||
- name: Build Application
|
||||
uses: ./.github/actions/build
|
||||
uses: $/.github/actions/build
|
||||
with:
|
||||
target: build-app
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
|
||||
@@ -31,17 +31,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
|
||||
- name: Build Demo
|
||||
uses: ./.github/actions/build
|
||||
uses: $/.github/actions/build
|
||||
with:
|
||||
target: build-demo
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: ./.github/actions/netlify-deploy
|
||||
uses: $/.github/actions/netlify-deploy
|
||||
with:
|
||||
dir: demo/dist
|
||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
@@ -62,17 +62,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
|
||||
- name: Build Demo
|
||||
uses: ./.github/actions/build
|
||||
uses: $/.github/actions/build
|
||||
with:
|
||||
target: build-demo
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: ./.github/actions/netlify-deploy
|
||||
uses: $/.github/actions/netlify-deploy
|
||||
with:
|
||||
dir: demo/dist
|
||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
|
||||
@@ -25,17 +25,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
|
||||
- name: Build Gallery
|
||||
uses: ./.github/actions/build
|
||||
uses: $/.github/actions/build
|
||||
with:
|
||||
target: build-gallery
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: ./.github/actions/netlify-deploy
|
||||
uses: $/.github/actions/netlify-deploy
|
||||
with:
|
||||
dir: gallery/dist
|
||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
|
||||
@@ -29,17 +29,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
|
||||
- name: Build Gallery
|
||||
uses: ./.github/actions/build
|
||||
uses: $/.github/actions/build
|
||||
with:
|
||||
target: build-gallery
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy preview to Netlify
|
||||
id: deploy
|
||||
uses: ./.github/actions/netlify-deploy
|
||||
uses: $/.github/actions/netlify-deploy
|
||||
with:
|
||||
dir: gallery/dist
|
||||
alias: deploy-preview-${{ github.event.number }}
|
||||
|
||||
+16
-16
@@ -32,13 +32,13 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare dependencies
|
||||
uses: ./.github/actions/prepare-dependencies
|
||||
uses: $/.github/actions/prepare-dependencies
|
||||
|
||||
prepare-container-dependencies:
|
||||
name: Prepare container dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
|
||||
options: --user 1001
|
||||
defaults:
|
||||
run:
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare dependencies
|
||||
uses: ./.github/actions/prepare-dependencies
|
||||
uses: $/.github/actions/prepare-dependencies
|
||||
with:
|
||||
node-modules-cache-key: node-modules-container-v1
|
||||
|
||||
@@ -66,12 +66,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Build demo
|
||||
uses: ./.github/actions/build
|
||||
uses: $/.github/actions/build
|
||||
with:
|
||||
target: build-demo-e2e
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -97,12 +97,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Build e2e test app
|
||||
uses: ./.github/actions/build
|
||||
uses: $/.github/actions/build
|
||||
with:
|
||||
target: build-e2e-test-app-e2e
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -128,12 +128,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Build gallery
|
||||
uses: ./.github/actions/build
|
||||
uses: $/.github/actions/build
|
||||
with:
|
||||
target: build-gallery
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
node-modules-cache-key: node-modules-container-v1
|
||||
@@ -207,7 +207,7 @@ jobs:
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
node-modules-cache-key: node-modules-container-v1
|
||||
@@ -261,7 +261,7 @@ jobs:
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
@@ -284,7 +284,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
node-modules-cache-key: node-modules-container-v1
|
||||
@@ -326,7 +326,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
immutable: false
|
||||
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
immutable: false
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
uses: home-assistant/actions/helpers/verify-version@cd6f3635a740c84d8a14f6121a90068b8ece6bf6 # master
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
immutable: false
|
||||
cache: false
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
with:
|
||||
immutable: false
|
||||
cache: false
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
|
||||
- name: Regenerate device class constants
|
||||
run: ./script/gen_device_classes
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
uses: $/.github/actions/setup
|
||||
|
||||
- name: Regenerate sensor entity constants
|
||||
run: ./script/gen_sensor_entity_constants
|
||||
|
||||
@@ -48,6 +48,36 @@ export const createMediaPlayerEntities = () => [
|
||||
group_members: ["media_player.playing", "media_player.stream_playing"],
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "media_player.music_all_features",
|
||||
state: "playing",
|
||||
attributes: {
|
||||
friendly_name: "Playing The Music (all features)",
|
||||
media_content_type: "music",
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + On + Off +
|
||||
// Play Media + Volume Step + Select Source + Stop + Clear + Play + Shuffle Set +
|
||||
// Sound Mode + Browse Media + Repeat Set + Grouping
|
||||
supported_features: 1048511,
|
||||
entity_picture: "/images/album_cover.jpg",
|
||||
media_duration: 300,
|
||||
media_position: 0,
|
||||
media_position_updated_at: new Date(
|
||||
// 23 seconds in
|
||||
new Date().getTime() - 23000
|
||||
).toISOString(),
|
||||
volume_level: 0.5,
|
||||
is_volume_muted: false,
|
||||
shuffle: false,
|
||||
repeat: "off",
|
||||
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
||||
source: "AirPlay",
|
||||
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
|
||||
sound_mode: "Music",
|
||||
group_members: ["media_player.playing", "media_player.stream_playing"],
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "media_player.stream_playing",
|
||||
state: "playing",
|
||||
|
||||
@@ -25,6 +25,13 @@ const CONFIGS = [
|
||||
entity: "media_player.music_playing",
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Playing Music (all features)",
|
||||
config: {
|
||||
type: "media-control",
|
||||
entity: "media_player.music_all_features",
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Playing Stream",
|
||||
config: {
|
||||
|
||||
+3
-3
@@ -78,8 +78,8 @@
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.23",
|
||||
"@tsparticles/engine": "4.4.0",
|
||||
"@tsparticles/preset-links": "4.4.0",
|
||||
"@tsparticles/engine": "4.5.0",
|
||||
"@tsparticles/preset-links": "4.5.0",
|
||||
"@versatiles/style": "7.0.1",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@vvo/tzdb": "6.201.1",
|
||||
@@ -154,7 +154,7 @@
|
||||
"@playwright/test": "1.64.1",
|
||||
"@rsdoctor/rspack-plugin": "1.6.5",
|
||||
"@rspack/core": "2.2.9",
|
||||
"@rspack/dev-server": "2.2.1",
|
||||
"@rspack/dev-server": "2.2.2",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "7.0.1",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
|
||||
Generated
+112
-112
@@ -295,11 +295,11 @@ importers:
|
||||
specifier: 0.5.23
|
||||
version: 0.5.23
|
||||
'@tsparticles/engine':
|
||||
specifier: 4.4.0
|
||||
version: 4.4.0
|
||||
specifier: 4.5.0
|
||||
version: 4.5.0
|
||||
'@tsparticles/preset-links':
|
||||
specifier: 4.4.0
|
||||
version: 4.4.0
|
||||
specifier: 4.5.0
|
||||
version: 4.5.0
|
||||
'@versatiles/style':
|
||||
specifier: 7.0.1
|
||||
version: 7.0.1([email protected])
|
||||
@@ -518,8 +518,8 @@ importers:
|
||||
specifier: 2.2.9
|
||||
version: 2.2.9(@swc/[email protected])
|
||||
'@rspack/dev-server':
|
||||
specifier: 2.2.1
|
||||
version: 2.2.1(@rspack/[email protected](@swc/[email protected]))
|
||||
specifier: 2.2.2
|
||||
version: 2.2.2(@rspack/[email protected](@swc/[email protected]))
|
||||
'@types/babel__plugin-transform-runtime':
|
||||
specifier: 7.9.5
|
||||
version: 7.9.5
|
||||
@@ -2990,8 +2990,8 @@ packages:
|
||||
'@swc/helpers':
|
||||
optional: true
|
||||
|
||||
'@rspack/[email protected].3':
|
||||
resolution: {integrity: sha512-GxnGj9jy76G3eCPyZei81fwKLAMLZaPEEqFz1/QDYquhwi/qYZX5fekFJ1XVpuwxGEK9KSX3hxZylfwrs4cmLA==}
|
||||
'@rspack/[email protected].6':
|
||||
resolution: {integrity: sha512-Rcx7dlDZQ9ddUJGuutFvnSeAKl+ZOc1fnC3htibs8yew3zMD9VgvSUe4MAwEn94Gb390l5heA+s/j5KhoSG5MQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
peerDependencies:
|
||||
'@rspack/core': ^2.0.0
|
||||
@@ -2999,8 +2999,8 @@ packages:
|
||||
'@rspack/core':
|
||||
optional: true
|
||||
|
||||
'@rspack/[email protected].1':
|
||||
resolution: {integrity: sha512-Il8HbYGK3rH5rM0XmUOsOGZVw69BKRpBZ61P28Fy8ry35CtfniDBWtbo/rvJtIT/sK0qwijmNrhF498ltsV+uA==}
|
||||
'@rspack/[email protected].2':
|
||||
resolution: {integrity: sha512-pK4V9YJW8dkfjy6WYJ2T6DrPkcIn2yjA1HiaU146141j8UCxHok5lsplyqudLzerOhvwqMrNFmnsuXLnFbfn4Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
peerDependencies:
|
||||
'@rspack/core': ^2.0.0
|
||||
@@ -3156,85 +3156,85 @@ packages:
|
||||
resolution: {integrity: sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@tsparticles/animation-utils@4.4.0':
|
||||
resolution: {integrity: sha512-yl4iE9SSuJ2n8B096t/vn4bWN+wWIkDm11OfTsIM3/64bJLB/5T92n6iOm/XLC3r6g8M8XuDi6ntTnpOcwsGxg==}
|
||||
'@tsparticles/animation-utils@4.5.0':
|
||||
resolution: {integrity: sha512-jIcPbhaA7X9gIEzUOsw63Vn7558ZLRbPeAodfzvIEo+VYhmnKOCrnf7e91THZ2XoFYjRQkkOt94uJfT8bma1TQ==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/basic@4.4.0':
|
||||
resolution: {integrity: sha512-lTbKBPzab0alPy+4Wm/xehrVpT1FrpEMaQcTU2eTV0c0G++5/kOINEWkC7GL7/cHovycukxB+tOMYVT54bSprA==}
|
||||
'@tsparticles/basic@4.5.0':
|
||||
resolution: {integrity: sha512-4yZucXSWNeHh5NXjJ1is4aiq1Fi8hW5gBTrlWbIm7JcqBjUxNkYCqdLKvOCR/5bnToqjtezBJWlArwBdbzuU6w==}
|
||||
|
||||
'@tsparticles/canvas-utils@4.4.0':
|
||||
resolution: {integrity: sha512-3zVr95OZ05BExgVYNT8WPqA/wFp83TiEJo66rZCZ/nZd1Ny8UCYLMKBcbUPBvGS2GB87UMK8+ygleZnlFhGTtw==}
|
||||
'@tsparticles/canvas-utils@4.5.0':
|
||||
resolution: {integrity: sha512-Yu/lxI9DzJvcvXSLlqEc7j/1B9f3A/Wa2yK6R+fn3ysDmf4CUVdjOiwa5Y65LlvvSpZMfBLbPoIgkwnCSbYaow==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/engine@4.4.0':
|
||||
resolution: {integrity: sha512-zUDykZ7qBqHYMXr6NveXg51kBlvfaQKMmWkMJ8nT7fC4dXdNayb1+TxOfAoqH9/U/u0Eyr0YE+72ai/ymS0uww==}
|
||||
'@tsparticles/engine@4.5.0':
|
||||
resolution: {integrity: sha512-ClhG2dxDU7HUAqCHGQZLgyIQRJFhuuCFCnNowSB9znuoNrxSeln4UKVBbRRv/994Eoi5OHuHuGaK9iw6ZeT21g==}
|
||||
|
||||
'@tsparticles/interaction-particles-links@4.4.0':
|
||||
resolution: {integrity: sha512-n2OkzB8BjQkzFrtiVq8O42/Tf+/Psng+K71MhJDI+czkwLo3PdmG51MCsy2w9ywCvGwWci2nwxMQ1qZL7nPONg==}
|
||||
'@tsparticles/interaction-particles-links@4.5.0':
|
||||
resolution: {integrity: sha512-L6F7eJdFVn3JsulaKrIwnl52rE9EazfMrNIUpa829PRoi/30BPOmRP469EKjdv3mntiwjUXgWFx8NDj+iQkJgw==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/plugin-interactivity': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
'@tsparticles/plugin-interactivity': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-blend@4.4.0':
|
||||
resolution: {integrity: sha512-X/SG2WIasgHqA+fLZFu6MMv3z5VY5uX4VIfKjXz1qxZjEjn0w4kzZrbtZBxXxRKdrm8Dg8Qu2mmBokNZ8Bny3g==}
|
||||
'@tsparticles/plugin-blend@4.5.0':
|
||||
resolution: {integrity: sha512-iTy4OajUDyDHyYVh2NaUZjKwzrjFfiGLiWl1I04dli417Ecmwfq/pU6leX/SIARVAqdvaeZOwFg4YmIN+zvv6w==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-hex-color@4.4.0':
|
||||
resolution: {integrity: sha512-sbRW6fi/YOS/hvFi6GCQiKI2zMCrmydrhlzqWyw2UUyg4Rr3FviQbIC2VRZrMc30Cm6kSkR3SovXIFDpZzO94Q==}
|
||||
'@tsparticles/plugin-hex-color@4.5.0':
|
||||
resolution: {integrity: sha512-yO3Rw27rR4ZpJo8IuBp0br1GXaju7iUl4u8zVD8WNcig7bbgoZqzhFhofgQYY9UPLVLyG1dnn8aODsk5Rq0X2g==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-hsl-color@4.4.0':
|
||||
resolution: {integrity: sha512-nw/7cGoU99y8veeYD7yA7ygNyrGr461V9xx+ed+jrj0LyXQbLF3d/r78ovMseoHxm4SWa9pmtoQxS3fBHIgecw==}
|
||||
'@tsparticles/plugin-hsl-color@4.5.0':
|
||||
resolution: {integrity: sha512-6AyFRVVXyHb4NrytzwKoCIUhSD/ZXIIr1av2z7cNgaLJCZ00qrur/G7FJhK1XUBNR4DV6/hYhP9ohSfrEhdZ+Q==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-interactivity@4.4.0':
|
||||
resolution: {integrity: sha512-jquoxlT4ra1Mc4glVlIfPubmchyIq5IZ25RdnvAop1uuc+6tu4N1zq5SSeyRO8MhJ94xevsNe6ozjmqvZ9CzKA==}
|
||||
'@tsparticles/plugin-interactivity@4.5.0':
|
||||
resolution: {integrity: sha512-Ydxd29SRldhjbvf5IIhu5NIfCAW2LdqSk6aN60zJb6AupV+wxztJ4/e4JL1t0ymj9992iQSVD9nQql4yzbdOwA==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-move@4.4.0':
|
||||
resolution: {integrity: sha512-o05YP6bWxljbxmsQoqbay8sn4GtHIiTQZhZmcIyxJw/q+oSCNKHWALElSAqa1H7Dp/7ltnmd4rH4pgbQtHnfEw==}
|
||||
'@tsparticles/plugin-move@4.5.0':
|
||||
resolution: {integrity: sha512-0lkYVygqc30SqBiuviI900fBKPHkJJyuZVpUBSolGSTz7J1P49PXKqy1az6f+ub42SZxFcGOChtQ5vwpZSh0Gg==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-rgb-color@4.4.0':
|
||||
resolution: {integrity: sha512-s7YlsrcjndVLiOgKNPQaPcEQmmZeZqHec0lpSbt8BgR2pQmjFvCMSzHDA/aaqZ6JX8iVqvj+bT8uqT5XQDq70Q==}
|
||||
'@tsparticles/plugin-rgb-color@4.5.0':
|
||||
resolution: {integrity: sha512-DBhtOO82K2kLaFginNEGfgfKoSXycj0eJzCHp2kSQ0HjKkpwINdK2nFh2QXfsYfjP1fQbLaIu3CuJG/VmjkDvw==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/preset-links@4.4.0':
|
||||
resolution: {integrity: sha512-skdPsg/3AgBFtv0vrn20KrPGbIa0AdZw812Avg86BW7NqVGeX3JLyowZbFRQTPU13CcUo9YXCeJccoZv6FovOw==}
|
||||
'@tsparticles/preset-links@4.5.0':
|
||||
resolution: {integrity: sha512-Itwr6ECwf3ORiroUvLAwwg8O66IT3tQ2CFdml6A0u3ze1kfBmL9JT/u9UYG4QFICT0qjE86pkXpFRv8NwWLkBw==}
|
||||
|
||||
'@tsparticles/shape-circle@4.4.0':
|
||||
resolution: {integrity: sha512-pCdCenLohHwaeeuMCw9bh1icGB+CKwBYx67L5dTBw6aig7LRvL+Kd1PKClJTgXkx8Mb1Xb7KLNmSYgh/ABRtEQ==}
|
||||
'@tsparticles/shape-circle@4.5.0':
|
||||
resolution: {integrity: sha512-6fyvghJFvUMokKIQU6p7KKwcX3pTnp6gizj/occVTnar8AxxOlXNjK1J019etwnqdLbSWU/E7Qdt7y4zCTPD1A==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-opacity@4.4.0':
|
||||
resolution: {integrity: sha512-StQAExNqeOHANRAMXHdaKKEBD1DrRwBZ3TQF6DPtERtoElj8BvMlPHq38W0TSzfvR0lFP91p2Pzo5txVzVJ3Gg==}
|
||||
'@tsparticles/updater-opacity@4.5.0':
|
||||
resolution: {integrity: sha512-808wb6KUe1wYzth/hJBMzcLDsApmES4FHOavoulyQmSJdWfiNED4LjoX2MNJg4ry56wtg9IXS7qa8XaqdekQYA==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-out-modes@4.4.0':
|
||||
resolution: {integrity: sha512-IzHaYhOVI6mBFqub1ivd/5UMkvn5Pb/j9xS8edT7XeSNYqO3UeomLJci0KE6Xsm+4K/kBLGbCRW5+Tf+kV4E0g==}
|
||||
'@tsparticles/updater-out-modes@4.5.0':
|
||||
resolution: {integrity: sha512-fOzj/2nUtwPK7A+1WN+EnQZtLPBI8++hxkgOHr+RIEGo2d9K4qOgoIxI1zpa6WHt9fSXCk4rWsjENpc++pB+JQ==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-paint@4.4.0':
|
||||
resolution: {integrity: sha512-v7liuaT9BwHsd1hCfJF2d93M2gD3lSfsfIfKjPihAYo73hS43Wq7Cf1F0yjEiR3yXryRGH1QmYYdhonKAvyDKw==}
|
||||
'@tsparticles/updater-paint@4.5.0':
|
||||
resolution: {integrity: sha512-BJK3De1UZW01uyIk5AJVW4ex2ejEfoU5Vqw512Ox/kGipx3qXsaROTbh/9+NbqMtRyDtYuDc5PgGWIWTyYb7TQ==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-size@4.4.0':
|
||||
resolution: {integrity: sha512-Qq8QjC0MMjlaHjtpWUROSqe14eKVphq92n0VR4/d4WY+pnSG1U1VJzF43Kyfb4IyV06b29F0TUROIG7Ivqk36g==}
|
||||
'@tsparticles/updater-size@4.5.0':
|
||||
resolution: {integrity: sha512-42AZGbnWUmIL7giRaerPauYHaX2Agx482+8dq3hBYvvc0vM4Pks/fhpaKtTCAGG4CJb3fvaiyRrF7T75CQVAdQ==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tufjs/[email protected]':
|
||||
resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
|
||||
@@ -10675,14 +10675,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@swc/helpers': 0.5.23
|
||||
|
||||
'@rspack/[email protected].3(@rspack/[email protected](@swc/[email protected]))':
|
||||
'@rspack/[email protected].6(@rspack/[email protected](@swc/[email protected]))':
|
||||
optionalDependencies:
|
||||
'@rspack/core': 2.2.9(@swc/[email protected])
|
||||
|
||||
'@rspack/[email protected].1(@rspack/[email protected](@swc/[email protected]))':
|
||||
'@rspack/[email protected].2(@rspack/[email protected](@swc/[email protected]))':
|
||||
dependencies:
|
||||
'@rspack/core': 2.2.9(@swc/[email protected])
|
||||
'@rspack/dev-middleware': 2.0.3(@rspack/[email protected](@swc/[email protected]))
|
||||
'@rspack/dev-middleware': 2.0.6(@rspack/[email protected](@swc/[email protected]))
|
||||
|
||||
'@rspack/[email protected]': {}
|
||||
|
||||
@@ -10844,88 +10844,88 @@ snapshots:
|
||||
magic-string: 0.30.21
|
||||
string.prototype.matchall: 4.0.12
|
||||
|
||||
'@tsparticles/animation-utils@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/animation-utils@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/basic@4.4.0':
|
||||
'@tsparticles/basic@4.5.0':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/plugin-blend': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/plugin-hex-color': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/plugin-hsl-color': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/plugin-move': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/plugin-rgb-color': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/shape-circle': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/updater-opacity': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/updater-out-modes': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/updater-paint': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/updater-size': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/engine': 4.5.0
|
||||
'@tsparticles/plugin-blend': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/plugin-hex-color': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/plugin-hsl-color': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/plugin-move': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/plugin-rgb-color': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/shape-circle': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/updater-opacity': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/updater-out-modes': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/updater-paint': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/updater-size': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
|
||||
'@tsparticles/canvas-utils@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/canvas-utils@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/engine@4.4.0': {}
|
||||
'@tsparticles/engine@4.5.0': {}
|
||||
|
||||
'@tsparticles/interaction-particles-links@4.4.0(@tsparticles/engine@4.4.0)(@tsparticles/plugin-interactivity@4.4.0(@tsparticles/engine@4.4.0))':
|
||||
'@tsparticles/interaction-particles-links@4.5.0(@tsparticles/engine@4.5.0)(@tsparticles/plugin-interactivity@4.5.0(@tsparticles/engine@4.5.0))':
|
||||
dependencies:
|
||||
'@tsparticles/canvas-utils': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/plugin-interactivity': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/canvas-utils': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/engine': 4.5.0
|
||||
'@tsparticles/plugin-interactivity': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
|
||||
'@tsparticles/plugin-blend@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/plugin-blend@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-hex-color@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/plugin-hex-color@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-hsl-color@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/plugin-hsl-color@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-interactivity@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/plugin-interactivity@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-move@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/plugin-move@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-rgb-color@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/plugin-rgb-color@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/preset-links@4.4.0':
|
||||
'@tsparticles/preset-links@4.5.0':
|
||||
dependencies:
|
||||
'@tsparticles/basic': 4.4.0
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/interaction-particles-links': 4.4.0(@tsparticles/engine@4.4.0)(@tsparticles/plugin-interactivity@4.4.0(@tsparticles/engine@4.4.0))
|
||||
'@tsparticles/plugin-interactivity': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/basic': 4.5.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
'@tsparticles/interaction-particles-links': 4.5.0(@tsparticles/engine@4.5.0)(@tsparticles/plugin-interactivity@4.5.0(@tsparticles/engine@4.5.0))
|
||||
'@tsparticles/plugin-interactivity': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
|
||||
'@tsparticles/shape-circle@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/shape-circle@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-opacity@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/updater-opacity@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/animation-utils': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/animation-utils': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-out-modes@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/updater-out-modes@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-paint@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/updater-paint@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-size@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/updater-size@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/animation-utils': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/animation-utils': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tufjs/[email protected]': {}
|
||||
|
||||
|
||||
+9
-1
@@ -59,12 +59,20 @@
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"description": "Keep gh-actions-lock used in CI up to date",
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/workflows/actions-lock\\.yaml$/"],
|
||||
"matchStrings": ["GH_ACTIONS_LOCK_VERSION: (?<currentValue>\\S+)"],
|
||||
"depNameTemplate": "github/gh-actions-lock",
|
||||
"datasourceTemplate": "github-releases"
|
||||
},
|
||||
{
|
||||
"description": "Keep Playwright CI container image up to date",
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/workflows/e2e\\.yaml$/"],
|
||||
"matchStrings": [
|
||||
"mcr\\.microsoft\\.com/playwright:(?<currentValue>v\\d+\\.\\d+\\.\\d+-noble)"
|
||||
"mcr\\.microsoft\\.com/playwright:(?<currentValue>v\\d+\\.\\d+\\.\\d+-noble)(?:@(?<currentDigest>sha256:[a-f0-9]{64}))?"
|
||||
],
|
||||
"depNameTemplate": "mcr.microsoft.com/playwright",
|
||||
"datasourceTemplate": "docker",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Return a copy of the list with the value in it, without adding duplicates
|
||||
* @param values - The current list
|
||||
* @param value - The value to add
|
||||
*/
|
||||
export const arrayWith = <T>(
|
||||
values: readonly T[] | undefined,
|
||||
value: T
|
||||
): T[] => (values?.includes(value) ? [...values] : [...(values ?? []), value]);
|
||||
|
||||
/**
|
||||
* Return a copy of the list without the value
|
||||
* @param values - The current list
|
||||
* @param value - The value to remove
|
||||
*/
|
||||
export const arrayWithout = <T>(
|
||||
values: readonly T[] | undefined,
|
||||
value: T
|
||||
): T[] => (values ?? []).filter((v) => v !== value);
|
||||
@@ -26,8 +26,10 @@ export interface QueryParamsControllerOptions {
|
||||
* Called when the page of the host is navigated to while the host is
|
||||
* connected, like a second link to an open page or coming back to it. Read
|
||||
* the URL with `searchParams()` and apply it, then `write()` the state back.
|
||||
* Gets the `popstate` or `location-changed` event, or nothing when the host
|
||||
* is connected again.
|
||||
*/
|
||||
navigated: () => void;
|
||||
navigated: (ev?: Event) => void;
|
||||
/**
|
||||
* The path of the host's page, for a page that can first be connected on
|
||||
* a sub-path it sends on to itself, like an add link. Defaults to the path
|
||||
@@ -159,7 +161,7 @@ export class QueryParamsController<
|
||||
}
|
||||
// Leaving the page gets here too, before the router swaps the page
|
||||
if (this._isActive()) {
|
||||
this._options.navigated();
|
||||
this._options.navigated(ev);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeCssColor } from "../color/compute-color";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { stateColorCss } from "./state_color";
|
||||
|
||||
export const ALERT_DOMAINS = [
|
||||
"alarm_control_panel",
|
||||
@@ -8,8 +10,6 @@ export const ALERT_DOMAINS = [
|
||||
"lock",
|
||||
];
|
||||
|
||||
export const DEFAULT_ALERT_COLOR = "red";
|
||||
|
||||
export const computeAlertActiveStates = (entityId: string): string[] => {
|
||||
switch (computeDomain(entityId)) {
|
||||
case "alarm_control_panel":
|
||||
@@ -25,3 +25,16 @@ export const computeAlertActiveStates = (entityId: string): string[] => {
|
||||
|
||||
export const isAlertActive = (stateObj: HassEntity): boolean =>
|
||||
computeAlertActiveStates(stateObj.entity_id).includes(stateObj.state);
|
||||
|
||||
export const computeAlertColor = (
|
||||
stateObj: HassEntity,
|
||||
color: string
|
||||
): string | undefined => {
|
||||
if (color === "none") {
|
||||
return undefined;
|
||||
}
|
||||
if (color === "state") {
|
||||
return stateColorCss(stateObj);
|
||||
}
|
||||
return computeCssColor(color);
|
||||
};
|
||||
@@ -29,9 +29,9 @@ import type {
|
||||
} from "../../common/translations/localize";
|
||||
import { formattersContext, registriesContext } from "../../data/context";
|
||||
import type { HomeAssistantFormatters, ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-button";
|
||||
import "../ha-button-toggle-group";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
@@ -121,6 +121,8 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
@query("ha-button.add") private _addButton?: HTMLElement;
|
||||
|
||||
private _editIndex?: number;
|
||||
|
||||
connectedCallback(): void {
|
||||
@@ -159,7 +161,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
${this.label ? html`<label id="label">${this.label}</label>` : nothing}
|
||||
${
|
||||
this.allowCustom
|
||||
? html`
|
||||
@@ -264,7 +266,9 @@ export class HaEntityNamePicker extends LitElement {
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
<ha-chip-set
|
||||
aria-labelledby=${ifDefined(this.label ? "label" : undefined)}
|
||||
>
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item,
|
||||
@@ -290,22 +294,16 @@ export class HaEntityNamePicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${
|
||||
this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this._localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`
|
||||
}
|
||||
<ha-button
|
||||
class="add"
|
||||
size="s"
|
||||
appearance="filled"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._addItem}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this._localize("ui.common.add")}
|
||||
</ha-button>
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
${this._renderRestoreButton()}
|
||||
@@ -373,6 +371,10 @@ export class HaEntityNamePicker extends LitElement {
|
||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
if (!value.length) {
|
||||
await this.updateComplete;
|
||||
this._addButton?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
@@ -629,7 +631,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -722,7 +724,8 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
ha-input-helper-text {
|
||||
display: block;
|
||||
margin: var(--ha-space-2) 0 0;
|
||||
padding: 0;
|
||||
margin: var(--ha-space-1) 0 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -245,15 +245,15 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
const newOrder: string[] = [];
|
||||
|
||||
for (const floorId of floorIds) {
|
||||
const isOnFloor = (areaId: string) =>
|
||||
(this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR) === floorId;
|
||||
if ((currentFloorId ?? UNASSIGNED_FLOOR) === floorId) {
|
||||
newHidden.push(...(value.hidden ?? []));
|
||||
// Reordering returns the hidden areas of every floor, not just this one
|
||||
newHidden.push(...(value.hidden ?? []).filter(isOnFloor));
|
||||
newOrder.push(...(value.order ?? []));
|
||||
continue;
|
||||
}
|
||||
const hidden = oldHidden.filter((areaId) => {
|
||||
const id = this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
return id === floorId;
|
||||
});
|
||||
const hidden = oldHidden.filter(isOnFloor);
|
||||
if (hidden?.length) {
|
||||
newHidden.push(...hidden);
|
||||
}
|
||||
|
||||
@@ -26,10 +26,7 @@ import "./ha-sortable";
|
||||
|
||||
type ClockDatePartSection = "weekday" | "day" | "month" | "year" | "separator";
|
||||
|
||||
type ClockDateSeparatorPart = Extract<
|
||||
ClockCardDatePart,
|
||||
"separator-dash" | "separator-slash" | "separator-dot" | "separator-new-line"
|
||||
>;
|
||||
type ClockDateSeparatorPart = Extract<ClockCardDatePart, `separator-${string}`>;
|
||||
|
||||
const CLOCK_DATE_PART_SECTION_ORDER: readonly ClockDatePartSection[] = [
|
||||
"day",
|
||||
@@ -43,6 +40,7 @@ const CLOCK_DATE_SEPARATOR_VALUES: Record<ClockDateSeparatorPart, string> = {
|
||||
"separator-dash": "-",
|
||||
"separator-slash": "/",
|
||||
"separator-dot": ".",
|
||||
"separator-comma": ",",
|
||||
"separator-new-line": "",
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -26,6 +28,10 @@ import "./chips/ha-input-chip";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import "./ha-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-input-helper-text";
|
||||
import type {
|
||||
PickerComboBoxItem,
|
||||
PickerComboBoxSearchFn,
|
||||
@@ -82,6 +88,8 @@ export class HaEntityTypePicker extends LitElement {
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states?: ContextType<typeof statesContext>;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
private _titlesLoaded = false;
|
||||
|
||||
protected willUpdate() {
|
||||
@@ -237,40 +245,13 @@ export class HaEntityTypePicker extends LitElement {
|
||||
const localize = this._i18n?.localize;
|
||||
const value = this.value ?? [];
|
||||
const chipLabels = localize ? this._chipLabels(localize, value) : undefined;
|
||||
|
||||
const addLabel = localize?.("ui.common.add");
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
${
|
||||
value.length && chipLabels
|
||||
? html`
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
value,
|
||||
(key) => key,
|
||||
(key) => {
|
||||
const label = chipLabels.get(key)!;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-key=${key}
|
||||
.label=${label}
|
||||
.disabled=${this.disabled}
|
||||
@remove=${this._removeItem}
|
||||
selected
|
||||
>
|
||||
${this._renderIcon(key, "icon")} ${label}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-chip-set>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${this.label ? html`<label id="label">${this.label}</label>` : nothing}
|
||||
<ha-generic-picker
|
||||
.helper=${this.helper}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.value=${""}
|
||||
.addButtonLabel=${localize?.("ui.components.entity-type-picker.add")}
|
||||
.getItems=${this._getItems}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.searchFn=${this._searchFn}
|
||||
@@ -278,10 +259,53 @@ export class HaEntityTypePicker extends LitElement {
|
||||
.emptyLabel=${localize?.("ui.components.entity-type-picker.no_types")}
|
||||
no-sort
|
||||
@value-changed=${this._itemAdded}
|
||||
></ha-generic-picker>
|
||||
>
|
||||
<ha-chip-set
|
||||
slot="field"
|
||||
aria-labelledby=${ifDefined(this.label ? "label" : undefined)}
|
||||
>
|
||||
${repeat(
|
||||
value,
|
||||
(key) => key,
|
||||
(key) => {
|
||||
const label = chipLabels?.get(key) ?? key;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-key=${key}
|
||||
.label=${label}
|
||||
.disabled=${this.disabled}
|
||||
@remove=${this._removeItem}
|
||||
selected
|
||||
>
|
||||
${this._renderIcon(key, "icon")} ${label}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
<ha-button
|
||||
size="s"
|
||||
appearance="filled"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._openPicker}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${addLabel}
|
||||
</ha-button>
|
||||
</ha-chip-set>
|
||||
</ha-generic-picker>
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _openPicker(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._picker?.open();
|
||||
}
|
||||
|
||||
private _itemAdded(ev: CustomEvent<{ value?: string }>) {
|
||||
ev.stopPropagation();
|
||||
const key = ev.detail.value;
|
||||
@@ -320,7 +344,15 @@ export class HaEntityTypePicker extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-2) 0;
|
||||
background-color: var(--mdc-text-field-fill-color);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-normal);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
ha-input-helper-text {
|
||||
padding: 0;
|
||||
margin-top: var(--ha-space-1);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -12,6 +11,11 @@ import {
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import type { Blueprints } from "../data/blueprint";
|
||||
@@ -19,11 +23,12 @@ import { fetchBlueprints } from "../data/blueprint";
|
||||
import { apiContext } from "../data/context";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
|
||||
@customElement("ha-filter-blueprints")
|
||||
export class HaFilterBlueprints extends LitElement {
|
||||
@@ -88,23 +93,30 @@ export class HaFilterBlueprints extends LitElement {
|
||||
${
|
||||
this._blueprints
|
||||
? html`
|
||||
<ha-list
|
||||
@selected=${this._blueprintsSelected}
|
||||
<ha-list-selectable
|
||||
multi
|
||||
class="ha-scrollbar"
|
||||
aria-label=${this._localize(
|
||||
"ui.panel.config.blueprint.caption"
|
||||
)}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
>
|
||||
${Object.entries(this._blueprints).map(
|
||||
([id, blueprint]) =>
|
||||
"error" in blueprint
|
||||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
: html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${id}
|
||||
.selected=${(this.value || []).includes(id)}
|
||||
.selected=${this.value?.includes(id) ?? false}
|
||||
>
|
||||
${blueprint.metadata.name || id}
|
||||
</ha-check-list-item>`
|
||||
<span slot="headline"
|
||||
>${blueprint.metadata.name || id}</span
|
||||
>
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-selectable>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
@@ -126,28 +138,20 @@ export class HaFilterBlueprints extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private async _blueprintsSelected(
|
||||
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||
private _handleItemToggled(
|
||||
ev: HASSDomEvent<number> & HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
) {
|
||||
const blueprints = this._blueprints!;
|
||||
|
||||
if (!ev.detail.index.size) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
const blueprintId = (
|
||||
ev.currentTarget.items[ev.detail] as HaListItemOption | undefined
|
||||
)?.value;
|
||||
if (!blueprintId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value: string[] = [];
|
||||
|
||||
for (const index of ev.detail.index) {
|
||||
const blueprintId = Object.keys(blueprints)[index];
|
||||
value.push(blueprintId);
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, blueprintId)
|
||||
: arrayWithout(this.value, blueprintId);
|
||||
}
|
||||
|
||||
private async _findRelated() {
|
||||
@@ -202,10 +206,14 @@ export class HaFilterBlueprints extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable::part(base) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -35,12 +35,12 @@ import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { showCategoryRegistryDetailDialog } from "../panels/config/category/show-dialog-category-registry-detail";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistantConnection } from "../types";
|
||||
import "./ha-button";
|
||||
import "./ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-list-item";
|
||||
import "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
|
||||
@@ -226,10 +226,14 @@ export class HaFilterCategories extends LitElement {
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
<ha-list-item graphic="icon" @click=${this._addCategory}>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon>
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
size="s"
|
||||
@click=${this._addCategory}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this._localize("ui.panel.config.category.editor.add")}
|
||||
</ha-list-item>
|
||||
</ha-button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
@@ -386,12 +390,12 @@ export class HaFilterCategories extends LitElement {
|
||||
ha-list-item-option {
|
||||
--ha-row-item-padding-block: var(--ha-space-2);
|
||||
}
|
||||
ha-button {
|
||||
margin: var(--ha-space-2) var(--ha-space-4);
|
||||
}
|
||||
ha-list-item-option ha-icon-button {
|
||||
--ha-icon-button-size: 32px;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
ha-dropdown-item {
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
@@ -85,6 +85,15 @@ export class HaFilterDevices extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(properties: PropertyValues<this>) {
|
||||
super.updated(properties);
|
||||
|
||||
// The rows don't change with the value, so render the selection again
|
||||
if (properties.has("value")) {
|
||||
this._listElement?.requestRowsUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
@@ -269,7 +278,6 @@ export class HaFilterDevices extends LitElement {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
this._listElement?.clearSelection();
|
||||
}
|
||||
|
||||
static styles = [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -13,17 +12,23 @@ import {
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext, statesContext } from "../data/context";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@@ -82,35 +87,38 @@ export class HaFilterDomains extends LitElement {
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@selected=${this._handleItemSelected}
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label=${this._localize("ui.panel.config.domains.caption")}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
>
|
||||
${repeat(
|
||||
this._domains(
|
||||
this._states,
|
||||
this._localize,
|
||||
this._i18n.locale.language,
|
||||
this._filter,
|
||||
this.value
|
||||
this._filter
|
||||
),
|
||||
(i) => i,
|
||||
(domain) =>
|
||||
html`<ha-check-list-item
|
||||
html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${domain}
|
||||
.selected=${(this.value || []).includes(domain)}
|
||||
graphic="icon"
|
||||
.selected=${this.value?.includes(domain) ?? false}
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
${domainToName(this._localize, domain)}
|
||||
</ha-check-list-item>`
|
||||
<span slot="headline"
|
||||
>${domainToName(this._localize, domain)}</span
|
||||
>
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-selectable>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
@@ -122,8 +130,7 @@ export class HaFilterDomains extends LitElement {
|
||||
states: ContextType<typeof statesContext>,
|
||||
localize: LocalizeFunc,
|
||||
language: string | undefined,
|
||||
filter: string | undefined,
|
||||
_value
|
||||
filter: string | undefined
|
||||
) => {
|
||||
const domains = new Set<string>();
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
@@ -150,22 +157,20 @@ export class HaFilterDomains extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleItemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
const domains = this._domains(
|
||||
this._states,
|
||||
this._localize,
|
||||
this._i18n.locale.language,
|
||||
this._filter,
|
||||
this.value
|
||||
);
|
||||
private _handleItemToggled(
|
||||
ev: HASSDomEvent<number> & HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
) {
|
||||
const domain = (
|
||||
ev.currentTarget.items[ev.detail] as HaListItemOption | undefined
|
||||
)?.value;
|
||||
if (!domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleDomains = new Set(domains);
|
||||
const preserved = (this.value || []).filter((d) => !visibleDomains.has(d));
|
||||
const selected = [...ev.detail.index]
|
||||
.map((i) => domains[i])
|
||||
.filter((d): d is string => !!d);
|
||||
|
||||
this.value = [...preserved, ...selected];
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, domain)
|
||||
: arrayWithout(this.value, domain);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value.length ? this.value : undefined,
|
||||
@@ -189,10 +194,14 @@ export class HaFilterDomains extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable::part(base) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -204,9 +213,6 @@ export class HaFilterDomains extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
@@ -25,16 +26,16 @@ import {
|
||||
} from "../data/context";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./ha-state-icon";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
import "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable-virtualized";
|
||||
import type { HaListSelectableVirtualized } from "./list/ha-list-selectable-virtualized";
|
||||
import type { HaListVirtualizedItem } from "./list/ha-list-virtualized";
|
||||
|
||||
interface EntityItem {
|
||||
interface EntityItem extends HaListVirtualizedItem {
|
||||
stateObj: HassEntity;
|
||||
name: string;
|
||||
}
|
||||
@@ -74,15 +75,14 @@ export class HaFilterEntities extends LitElement {
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
|
||||
@query("ha-list-selectable-virtualized")
|
||||
private _list?: HaListSelectableVirtualized;
|
||||
|
||||
private _relatedRequestId = 0;
|
||||
|
||||
public willUpdate(properties: PropertyValues<this>) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
}
|
||||
|
||||
if (
|
||||
properties.has("value") &&
|
||||
!deepEqual(this.value, properties.get("value"))
|
||||
@@ -91,6 +91,15 @@ export class HaFilterEntities extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(properties: PropertyValues<this>) {
|
||||
super.updated(properties);
|
||||
|
||||
// The rows don't change with the value, so render the selection again
|
||||
if (properties.has("value")) {
|
||||
this._list?.requestRowsUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
@@ -121,23 +130,14 @@ export class HaFilterEntities extends LitElement {
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._entities(
|
||||
this._states,
|
||||
this._formatters,
|
||||
this.type,
|
||||
this._filter || "",
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${this._renderItem}
|
||||
@click=${this._handleItemClick}
|
||||
@keydown=${this._handleItemKeydown}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</ha-list>
|
||||
<ha-list-selectable-virtualized
|
||||
multi
|
||||
aria-label=${this._localize("ui.panel.config.entities.caption")}
|
||||
.rows=${this._currentEntities()}
|
||||
.rowRenderer=${this._renderItem}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
></ha-list-selectable-virtualized>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
@@ -145,43 +145,43 @@ export class HaFilterEntities extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _keyFunction = (item?: EntityItem) => item?.stateObj.entity_id;
|
||||
|
||||
private _renderItem = (item?: EntityItem) =>
|
||||
!item
|
||||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
tabindex="0"
|
||||
.value=${item.stateObj.entity_id}
|
||||
.selected=${this.value?.includes(item.stateObj.entity_id) ?? false}
|
||||
graphic="icon"
|
||||
: html`<ha-list-item-option
|
||||
style="width: 100%;"
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${item.id}
|
||||
.selected=${this.value?.includes(item.id) ?? false}
|
||||
>
|
||||
<ha-state-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.stateObj=${item.stateObj}
|
||||
></ha-state-icon>
|
||||
${item.name}
|
||||
</ha-check-list-item>`;
|
||||
<span slot="headline">${item.name}</span>
|
||||
</ha-list-item-option>`;
|
||||
|
||||
private _handleItemKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
this._handleItemClick(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
const listItem = ev.target.closest("ha-check-list-item");
|
||||
const value = listItem?.value;
|
||||
if (!value) {
|
||||
private _handleItemToggled(ev: CustomEvent<number>) {
|
||||
const entityId = this._currentEntities()[ev.detail]?.id;
|
||||
if (!entityId) {
|
||||
return;
|
||||
}
|
||||
if (this.value?.includes(value)) {
|
||||
this.value = this.value?.filter((val) => val !== value);
|
||||
} else {
|
||||
this.value = [...(this.value || []), value];
|
||||
}
|
||||
listItem.selected = this.value?.includes(value);
|
||||
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, entityId)
|
||||
: arrayWithout(this.value, entityId);
|
||||
}
|
||||
|
||||
private _currentEntities() {
|
||||
return this._entities(
|
||||
this._states,
|
||||
this._formatters,
|
||||
this.type,
|
||||
this._filter || "",
|
||||
this._i18n.locale.language
|
||||
);
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
@@ -199,12 +199,13 @@ export class HaFilterEntities extends LitElement {
|
||||
formatters: ContextType<typeof formattersContext>,
|
||||
type: this["type"],
|
||||
filter: string,
|
||||
language: string | undefined,
|
||||
_value
|
||||
language: string | undefined
|
||||
): EntityItem[] =>
|
||||
Object.values(states)
|
||||
.filter((stateObj) => !type || computeStateDomain(stateObj) !== type)
|
||||
.map((stateObj) => ({
|
||||
id: stateObj.entity_id,
|
||||
interactive: true,
|
||||
stateObj,
|
||||
name: formatters.formatEntityName(stateObj),
|
||||
}))
|
||||
@@ -267,10 +268,9 @@ export class HaFilterEntities extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable-virtualized {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -298,10 +298,6 @@ export class HaFilterEntities extends LitElement {
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
@@ -13,17 +12,23 @@ import {
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext, manifestsContext } from "../data/context";
|
||||
import type { IntegrationManifest } from "../data/integration";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@@ -92,37 +97,41 @@ export class HaFilterIntegrations extends LitElement {
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@selected=${this._itemSelected}
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label=${this._localize(
|
||||
"ui.panel.config.integrations.caption"
|
||||
)}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
>
|
||||
${repeat(
|
||||
this._integrations(
|
||||
this._localize,
|
||||
manifests,
|
||||
this._filter,
|
||||
this.value,
|
||||
this._i18n.locale.language
|
||||
),
|
||||
(i) => i.domain,
|
||||
(integration) =>
|
||||
html`<ha-check-list-item
|
||||
html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${integration.domain}
|
||||
.selected=${(this.value || []).includes(
|
||||
integration.domain
|
||||
)}
|
||||
graphic="icon"
|
||||
.selected=${
|
||||
this.value?.includes(integration.domain) ??
|
||||
false
|
||||
}
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.domain=${integration.domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
${integration.name}
|
||||
</ha-check-list-item>`
|
||||
<span slot="headline">${integration.name}</span>
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list>`
|
||||
</ha-list-selectable>`
|
||||
: nothing
|
||||
}
|
||||
</div>`
|
||||
@@ -144,7 +153,6 @@ export class HaFilterIntegrations extends LitElement {
|
||||
localize: LocalizeFunc,
|
||||
manifest: IntegrationManifest[],
|
||||
filter: string | undefined,
|
||||
_value: string[] | undefined,
|
||||
language: string
|
||||
) =>
|
||||
manifest
|
||||
@@ -165,26 +173,20 @@ export class HaFilterIntegrations extends LitElement {
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
);
|
||||
|
||||
private _itemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
if (!this._manifests) {
|
||||
private _handleItemToggled(
|
||||
ev: HASSDomEvent<number> & HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
) {
|
||||
const domain = (
|
||||
ev.currentTarget.items[ev.detail] as HaListItemOption | undefined
|
||||
)?.value;
|
||||
if (!domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
const integrations = this._integrations(
|
||||
this._localize,
|
||||
this._manifestList(this._manifests),
|
||||
this._filter,
|
||||
this.value,
|
||||
this._i18n.locale.language
|
||||
);
|
||||
|
||||
const visibleDomains = new Set(integrations.map((i) => i.domain));
|
||||
const preserved = (this.value || []).filter((d) => !visibleDomains.has(d));
|
||||
const selected = [...ev.detail.index]
|
||||
.map((i) => integrations[i]?.domain)
|
||||
.filter((d): d is string => !!d);
|
||||
|
||||
this.value = [...preserved, ...selected];
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, domain)
|
||||
: arrayWithout(this.value, domain);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value.length ? this.value : undefined,
|
||||
@@ -208,10 +210,14 @@ export class HaFilterIntegrations extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable::part(base) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -223,9 +229,6 @@ export class HaFilterIntegrations extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiCog, mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -13,22 +12,29 @@ import {
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext, labelsContext } from "../data/context";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
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 "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
import "./item/ha-list-item-button";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-base";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
|
||||
@customElement("ha-filter-labels")
|
||||
export class HaFilterLabels extends LitElement {
|
||||
@@ -57,12 +63,10 @@ export class HaFilterLabels extends LitElement {
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
|
||||
private _filteredLabels = memoizeOne(
|
||||
// `_value` used to recalculate the memoization when the selection changes
|
||||
(
|
||||
labels: LabelRegistryEntry[],
|
||||
filter: string | undefined,
|
||||
language: string | undefined,
|
||||
_value
|
||||
language: string | undefined
|
||||
) =>
|
||||
labels
|
||||
.filter(
|
||||
@@ -105,26 +109,28 @@ export class HaFilterLabels extends LitElement {
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
@selected=${this._labelSelected}
|
||||
class="ha-scrollbar"
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label=${this._localize("ui.panel.config.labels.caption")}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
>
|
||||
${repeat(
|
||||
this._filteredLabels(
|
||||
this._labels || [],
|
||||
this._filter,
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
this._i18n.locale.language
|
||||
),
|
||||
(label) => label.label_id,
|
||||
(label) =>
|
||||
html`<ha-check-list-item
|
||||
html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${label.label_id}
|
||||
.selected=${(this.value || []).includes(label.label_id)}
|
||||
hasMeta
|
||||
.selected=${this.value?.includes(label.label_id) ?? false}
|
||||
>
|
||||
<ha-label
|
||||
slot="headline"
|
||||
.color=${label.color}
|
||||
.description=${label.description}
|
||||
>
|
||||
@@ -138,13 +144,17 @@ export class HaFilterLabels extends LitElement {
|
||||
}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-check-list-item>`
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list>
|
||||
<ha-list-item graphic="icon" @click=${this._manageLabels}>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||
${this._localize("ui.panel.config.labels.manage_labels")}
|
||||
</ha-list-item>
|
||||
</ha-list-selectable>
|
||||
<ha-list-base>
|
||||
<ha-list-item-button @click=${this._manageLabels}>
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
<span slot="headline"
|
||||
>${this._localize("ui.panel.config.labels.manage_labels")}</span
|
||||
>
|
||||
</ha-list-item-button>
|
||||
</ha-list-base>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
@@ -164,35 +174,23 @@ export class HaFilterLabels extends LitElement {
|
||||
this._filter = value.toLowerCase();
|
||||
}
|
||||
|
||||
private async _labelSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
const filteredLabels = this._filteredLabels(
|
||||
this._labels || [],
|
||||
this._filter,
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
);
|
||||
|
||||
const filteredLabelIds = new Set(filteredLabels.map((l) => l.label_id));
|
||||
|
||||
// Keep previously selected labels that are not in the current filtered view
|
||||
const preservedLabels = (this.value || []).filter(
|
||||
(id) => !filteredLabelIds.has(id)
|
||||
);
|
||||
|
||||
// Build the new selection from the filtered labels based on selected indices
|
||||
const newlySelectedLabels: string[] = [];
|
||||
for (const index of ev.detail.index) {
|
||||
const labelId = filteredLabels[index]?.label_id;
|
||||
if (labelId) {
|
||||
newlySelectedLabels.push(labelId);
|
||||
}
|
||||
private _handleItemToggled(
|
||||
ev: HASSDomEvent<number> & HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
) {
|
||||
const labelId = (
|
||||
ev.currentTarget.items[ev.detail] as HaListItemOption | undefined
|
||||
)?.value;
|
||||
if (!labelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = [...preservedLabels, ...newlySelectedLabels];
|
||||
this.value = value.length ? value : [];
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, labelId)
|
||||
: arrayWithout(this.value, labelId);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: value.length ? value : undefined,
|
||||
value: this.value.length ? this.value : undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
@@ -208,10 +206,14 @@ export class HaFilterLabels extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable::part(base) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
|
||||
@customElement("ha-filter-states")
|
||||
export class HaFilterStates extends LitElement {
|
||||
@@ -40,7 +46,6 @@ export class HaFilterStates extends LitElement {
|
||||
if (!this.states) {
|
||||
return nothing;
|
||||
}
|
||||
const hasIcon = this.states.find((item) => item.icon);
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
@@ -64,30 +69,32 @@ export class HaFilterStates extends LitElement {
|
||||
this._panel.showContent
|
||||
? html`
|
||||
<div class="content" ${ref(this._content)}>
|
||||
<ha-list
|
||||
@selected=${this._statesSelected}
|
||||
<ha-list-selectable
|
||||
multi
|
||||
class="ha-scrollbar"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
>
|
||||
${this.states.map(
|
||||
(item) =>
|
||||
html`<ha-check-list-item
|
||||
html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${item.value}
|
||||
.selected=${this.value?.includes(item.value) ?? false}
|
||||
.graphic=${hasIcon ? "icon" : null}
|
||||
>
|
||||
${
|
||||
item.icon
|
||||
? html`<ha-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.icon=${item.icon}
|
||||
></ha-icon>`
|
||||
: nothing
|
||||
}
|
||||
${item.label}
|
||||
</ha-check-list-item>`
|
||||
<span slot="headline">${item.label}</span>
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-selectable>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
@@ -99,26 +106,23 @@ export class HaFilterStates extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private async _statesSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
if (!ev.detail.index.size) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
private _handleItemToggled(
|
||||
ev: HASSDomEvent<number> & HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
) {
|
||||
const state = (
|
||||
ev.currentTarget.items[ev.detail] as HaListItemOption | undefined
|
||||
)?.value;
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value: string[] = [];
|
||||
|
||||
for (const index of ev.detail.index) {
|
||||
const val = this.states![index].value;
|
||||
value.push(val);
|
||||
}
|
||||
this.value = value;
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, state)
|
||||
: arrayWithout(this.value, state);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value,
|
||||
value: this.value,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
@@ -134,10 +138,14 @@ export class HaFilterStates extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable::part(base) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -11,14 +10,19 @@ import {
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-label";
|
||||
import "./ha-list";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
import "./voice-assistant-brand-icon";
|
||||
import { voiceAssistants } from "../data/expose";
|
||||
|
||||
@@ -64,30 +68,35 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
${
|
||||
this._panel.showContent
|
||||
? html`<div class="content" ${ref(this._content)}>
|
||||
<ha-list
|
||||
@selected=${this._assistantsSelected}
|
||||
class="ha-scrollbar"
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label=${this._localize(
|
||||
"ui.panel.config.dashboard.voice_assistants.main"
|
||||
)}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
>
|
||||
${repeat(
|
||||
this._voiceAssistantOptions,
|
||||
(voiceAssistantId) => voiceAssistantId,
|
||||
(voiceAssistantId) =>
|
||||
html`<ha-check-list-item
|
||||
html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${voiceAssistantId}
|
||||
.selected=${(this.value || []).includes(voiceAssistantId)}
|
||||
hasMeta
|
||||
graphic="icon"
|
||||
.selected=${this.value?.includes(voiceAssistantId) ?? false}
|
||||
>
|
||||
<voice-assistant-brand-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.voiceAssistantId=${voiceAssistantId}
|
||||
>
|
||||
</voice-assistant-brand-icon>
|
||||
${voiceAssistants[voiceAssistantId].name}
|
||||
</ha-check-list-item>`
|
||||
<span slot="headline"
|
||||
>${voiceAssistants[voiceAssistantId].name}</span
|
||||
>
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-selectable>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
@@ -103,23 +112,20 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private async _assistantsSelected(
|
||||
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||
private _handleItemToggled(
|
||||
ev: HASSDomEvent<number> & HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
) {
|
||||
if (!ev.detail.index) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
const voiceAssistantId = (
|
||||
ev.currentTarget.items[ev.detail] as HaListItemOption | undefined
|
||||
)?.value;
|
||||
if (!voiceAssistantId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newvalue: string[] = [];
|
||||
for (const index of ev.detail.index) {
|
||||
newvalue.push(this._voiceAssistantOptions![index]);
|
||||
}
|
||||
this.value = newvalue;
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, voiceAssistantId)
|
||||
: arrayWithout(this.value, voiceAssistantId);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value,
|
||||
@@ -138,10 +144,14 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable::part(base) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -153,9 +163,6 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { mdiDragHorizontalVariant } from "@mdi/js";
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import type { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../../types";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-checkbox";
|
||||
import "../ha-dropdown-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-button";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-select";
|
||||
import "../ha-select-box";
|
||||
import "../ha-sortable";
|
||||
import "../ha-svg-icon";
|
||||
import "../radio/ha-radio-group";
|
||||
import "../radio/ha-radio-option";
|
||||
|
||||
@@ -28,10 +30,7 @@ import "../radio/ha-radio-option";
|
||||
export class HaSelectSelector extends LitElement {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale!: FrontendLocaleData;
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
|
||||
@property({ attribute: false }) public selector!: SelectSelector;
|
||||
|
||||
@@ -48,6 +47,8 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
private _itemMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
@@ -83,7 +84,11 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
if (this.selector.select?.sort) {
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.label, b.label, this._locale.language)
|
||||
caseInsensitiveStringCompare(
|
||||
a.label,
|
||||
b.label,
|
||||
this._i18n.locale.language
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,67 +163,80 @@ export class HaSelectSelector extends LitElement {
|
||||
const value =
|
||||
!this.value || this.value === "" ? [] : ensureArray(this.value);
|
||||
|
||||
const addLabel = this._i18n.localize("ui.common.add");
|
||||
|
||||
return html`
|
||||
${
|
||||
value?.length
|
||||
? html`
|
||||
<ha-sortable
|
||||
no-style
|
||||
.disabled=${!this.selector.select.reorder}
|
||||
@item-moved=${this._itemMoved}
|
||||
handle-selector="button.primary.action"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
value,
|
||||
(item) => item,
|
||||
(item, idx) => {
|
||||
const label =
|
||||
options.find((option) => option.value === item)
|
||||
?.label || item;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
.idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
.label=${label}
|
||||
.title=${label}
|
||||
selected
|
||||
>
|
||||
${
|
||||
this.selector.select?.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
options.find((option) => option.value === item)
|
||||
?.label || item
|
||||
}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
`
|
||||
this.label
|
||||
? html`<span id="label" class="label chips-label"
|
||||
>${this.label}</span
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<ha-generic-picker
|
||||
no-sort
|
||||
.helper=${this.helper}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.value=${""}
|
||||
.addButtonLabel=${this.label}
|
||||
.getItems=${this._getItems(options, value, true)}
|
||||
.allowCustomValue=${this.selector.select.custom_value ?? false}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
></ha-generic-picker>
|
||||
>
|
||||
<ha-sortable
|
||||
slot="field"
|
||||
no-style
|
||||
.disabled=${!this.selector.select.reorder}
|
||||
@item-moved=${this._itemMoved}
|
||||
draggable-selector="ha-input-chip"
|
||||
handle-selector="button.primary.action"
|
||||
>
|
||||
<ha-chip-set
|
||||
aria-labelledby=${ifDefined(this.label ? "label" : undefined)}
|
||||
>
|
||||
${repeat(
|
||||
value,
|
||||
(item) => item,
|
||||
(item, idx) => {
|
||||
const label =
|
||||
options.find((option) => option.value === item)?.label ||
|
||||
item;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
.idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
.label=${label}
|
||||
.title=${label}
|
||||
selected
|
||||
>
|
||||
${
|
||||
this.selector.select?.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${label}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
<ha-button
|
||||
size="s"
|
||||
appearance="filled"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._openPicker}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${addLabel}
|
||||
</ha-button>
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</ha-generic-picker>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -263,6 +281,11 @@ export class HaSelectSelector extends LitElement {
|
||||
: "";
|
||||
}
|
||||
|
||||
private _openPicker(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._picker?.open();
|
||||
}
|
||||
|
||||
private _getOptions = memoizeOne(
|
||||
(selector: SelectSelector) =>
|
||||
selector.select?.options?.map((option) =>
|
||||
@@ -403,19 +426,32 @@ export class HaSelectSelector extends LitElement {
|
||||
ha-dropdown-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-chip-set {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.chips-label {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
|
||||
ha-sortable ha-chip-set {
|
||||
background-color: var(--mdc-text-field-fill-color);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-normal);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-select-box + ha-input-helper-text {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
ha-generic-picker + ha-input-helper-text {
|
||||
padding: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.checkbox-list ha-input-helper-text {
|
||||
padding: 0;
|
||||
margin-top: 4px;
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { resolveTimeZone } from "../../common/datetime/resolve-time-zone";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { configContext, internationalizationContext } from "../../data/context";
|
||||
import type { UiClockDateFormatSelector } from "../../data/selector";
|
||||
import {
|
||||
CLOCK_CARD_DATE_PRESETS,
|
||||
formatClockCardDatePreset,
|
||||
getClockCardDatePresetParts,
|
||||
isClockCardDatePreset,
|
||||
} from "../../panels/lovelace/cards/clock/clock-date-format";
|
||||
import type { ClockCardDatePreset } from "../../panels/lovelace/cards/types";
|
||||
import "../ha-clock-date-format-picker";
|
||||
import "../ha-select";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "../ha-select";
|
||||
|
||||
type ClockDateFormatMode = ClockCardDatePreset | "custom";
|
||||
|
||||
@customElement("ha-selector-ui_clock_date_format")
|
||||
export class HaSelectorUiClockDateFormat extends LitElement {
|
||||
@@ -17,17 +35,130 @@ export class HaSelectorUiClockDateFormat extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig!: ContextType<typeof configContext>;
|
||||
|
||||
@state() private _custom = false;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (
|
||||
changedProperties.has("value") &&
|
||||
Array.isArray(this.value) &&
|
||||
this.value.length
|
||||
) {
|
||||
this._custom = true;
|
||||
}
|
||||
}
|
||||
|
||||
private get _mode(): ClockDateFormatMode | undefined {
|
||||
if (isClockCardDatePreset(this.value)) {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
return this._custom ? "custom" : undefined;
|
||||
}
|
||||
|
||||
private _options = memoizeOne(
|
||||
(
|
||||
i18n: ContextType<typeof internationalizationContext>,
|
||||
serverTimeZone: string
|
||||
): HaSelectOption[] => {
|
||||
const now = new Date();
|
||||
const timeZone = resolveTimeZone(i18n.locale.time_zone, serverTimeZone);
|
||||
|
||||
return [
|
||||
...CLOCK_CARD_DATE_PRESETS.map((preset) => ({
|
||||
value: preset,
|
||||
label: i18n.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.date.presets.${preset}`
|
||||
),
|
||||
secondary: formatClockCardDatePreset(
|
||||
now,
|
||||
preset,
|
||||
i18n.locale.language,
|
||||
timeZone
|
||||
),
|
||||
})),
|
||||
{
|
||||
value: "custom",
|
||||
label: i18n.localize(
|
||||
"ui.panel.lovelace.editor.card.clock.date.presets.custom"
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const mode = this._mode;
|
||||
|
||||
return html`
|
||||
<ha-clock-date-format-picker
|
||||
.value=${this.value}
|
||||
<ha-select
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.value=${mode}
|
||||
.options=${this._options(this._i18n, this._hassConfig.config.time_zone)}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-clock-date-format-picker>
|
||||
.clearable=${!this.required}
|
||||
@selected=${this._modeSelected}
|
||||
></ha-select>
|
||||
${
|
||||
mode === "custom"
|
||||
? html`
|
||||
<ha-clock-date-format-picker
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-clock-date-format-picker>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _modeSelected(
|
||||
ev: HaSelectSelectEvent<ClockDateFormatMode, true>
|
||||
): void {
|
||||
ev.stopPropagation();
|
||||
const mode = ev.detail.value;
|
||||
const currentValue = this.value;
|
||||
|
||||
this._custom = mode === "custom";
|
||||
|
||||
if (mode === "custom") {
|
||||
if (isClockCardDatePreset(currentValue)) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: getClockCardDatePresetParts(
|
||||
currentValue,
|
||||
this._i18n.locale.language
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: mode });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
ha-select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -98,8 +98,9 @@ export class HaUnitOfMeasurementSelector extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.required && units.length === 1 && units[0] !== null) {
|
||||
// Preselect the only allowed unit
|
||||
if (units.length === 1 && units[0] !== null) {
|
||||
// Preselect the only allowed unit; "no unit" is not allowed either, so
|
||||
// this also applies to an optional field
|
||||
fireEvent(this, "value-changed", { value: units[0] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,6 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
}
|
||||
|
||||
.pane .footer {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -70,6 +70,20 @@ export class HaListItemBase extends HaRowItem {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>): void {
|
||||
super.updated(changed);
|
||||
// The list only reads focusability on (un)registration, so tell it when
|
||||
// the row starts or stops taking focus later on
|
||||
if (
|
||||
this._list &&
|
||||
((changed.has("disabled") && changed.get("disabled") !== undefined) ||
|
||||
(changed.has("interactive") &&
|
||||
changed.get("interactive") !== undefined))
|
||||
) {
|
||||
this._list.requestListItemsUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the item (Enter/Space from the parent list). Default dispatches
|
||||
* a click on the host. Subclasses that wrap a native element (e.g. `<a>`)
|
||||
|
||||
@@ -71,6 +71,8 @@ export class HaListBase extends LitElement {
|
||||
|
||||
private _unbindKeys?: () => void;
|
||||
|
||||
private _listItemsUpdatePending = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.hasAttribute("ha-list")) {
|
||||
@@ -170,6 +172,21 @@ export class HaListBase extends LitElement {
|
||||
this.applyActive(focusItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `updateListItems()` once after the current task, so rows that change
|
||||
* their focusability together cause a single update.
|
||||
*/
|
||||
public requestListItemsUpdate() {
|
||||
if (this._listItemsUpdatePending) {
|
||||
return;
|
||||
}
|
||||
this._listItemsUpdatePending = true;
|
||||
queueMicrotask(() => {
|
||||
this._listItemsUpdatePending = false;
|
||||
this.updateListItems();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook called whenever the items array has changed. Subclasses can override
|
||||
* to layer in extra bookkeeping (e.g. selection state sync).
|
||||
@@ -191,12 +208,12 @@ export class HaListBase extends LitElement {
|
||||
) {
|
||||
this.activeItemIndex = -1;
|
||||
}
|
||||
} else if (
|
||||
this.activeItemIndex >= this.itemCount ||
|
||||
!this.hasFocusableItem ||
|
||||
this.activeItemIndex < 0
|
||||
) {
|
||||
} else if (!this.isFocusable(this.activeItemIndex)) {
|
||||
// Also leave an active row that got disabled, so the list keeps a Tab stop
|
||||
this.activeItemIndex = this.firstFocusableIndex;
|
||||
if (activeIndex !== -1) {
|
||||
this._activeItem = this.items[this.activeItemIndex];
|
||||
}
|
||||
}
|
||||
if (!this._activeItem) {
|
||||
this._activeItem = this.items[this.activeItemIndex];
|
||||
|
||||
@@ -85,17 +85,21 @@ export class HaListVirtualized extends HaListBase {
|
||||
|
||||
protected rangeStart = -1;
|
||||
protected rangeEnd = -1;
|
||||
private _visibleStart = -1;
|
||||
private _visibleEnd = -1;
|
||||
private _activeItemFocus = false;
|
||||
private _scrollToActiveItem = false;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
if (!this.hasUpdated) {
|
||||
this._loadVirtualizer();
|
||||
}
|
||||
|
||||
if (changedProps.has("rows")) {
|
||||
this.recomputeFocusableIndexes();
|
||||
this.activeItemIndex = this.virtualFocus ? -1 : this.firstFocusableIndex;
|
||||
this.activeItemIndex = this.virtualFocus
|
||||
? -1
|
||||
: this._followActiveRow(changedProps.get("rows"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +108,28 @@ export class HaListVirtualized extends HaListBase {
|
||||
return this.virtualizerElement ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the rows in view again. Call it when `rowRenderer` reads state
|
||||
* that is not part of `rows`, like the selection.
|
||||
*/
|
||||
public requestRowsUpdate() {
|
||||
this.virtualizerElement?.requestUpdate();
|
||||
}
|
||||
|
||||
// Keep the active row if the list has focus, else reset to the first row
|
||||
private _followActiveRow(previousRows?: HaListVirtualizedItem[]): number {
|
||||
const activeId = this.matches(":focus-within")
|
||||
? previousRows?.[this.activeItemIndex]?.id
|
||||
: undefined;
|
||||
const index =
|
||||
activeId === undefined
|
||||
? -1
|
||||
: this.rows.findIndex((row) => row.id === activeId);
|
||||
return index !== -1 && this.isFocusable(index)
|
||||
? index
|
||||
: this.firstFocusableIndex;
|
||||
}
|
||||
|
||||
/** Scroll the row at `index` into view. */
|
||||
public scrollToIndex(
|
||||
index: number,
|
||||
@@ -196,20 +222,41 @@ export class HaListVirtualized extends HaListBase {
|
||||
return;
|
||||
}
|
||||
if (this.virtualizerElement && this.rangeStart > -1) {
|
||||
const tabStopIndex = this._tabStopIndex();
|
||||
Array.from(this.virtualizerElement.children).forEach((child, index) => {
|
||||
const el = child as HTMLElement;
|
||||
if (index + this.rangeStart === this.activeItemIndex) {
|
||||
el.tabIndex = 0;
|
||||
if (focusItem) {
|
||||
el.focus();
|
||||
}
|
||||
} else {
|
||||
const rowIndex = index + this.rangeStart;
|
||||
// Like the base list, keep other rows focusable by click
|
||||
if (!this.isFocusable(rowIndex)) {
|
||||
el.removeAttribute("tabindex");
|
||||
return;
|
||||
}
|
||||
el.tabIndex = rowIndex === tabStopIndex ? 0 : -1;
|
||||
if (focusItem && rowIndex === this.activeItemIndex) {
|
||||
el.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The row that takes Tab: the active row while it is visible, else the
|
||||
// first visible row, so Tab never lands on a row out of view
|
||||
private _tabStopIndex(): number {
|
||||
const [start, end] =
|
||||
this._visibleStart > -1
|
||||
? [this._visibleStart, this._visibleEnd]
|
||||
: [this.rangeStart, this.rangeEnd];
|
||||
if (this.activeItemIndex >= start && this.activeItemIndex <= end) {
|
||||
return this.activeItemIndex;
|
||||
}
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (this.isFocusable(i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private _applyVirtualActive(reveal: boolean) {
|
||||
if (!this.virtualizerElement || this.rangeStart < 0) {
|
||||
return;
|
||||
@@ -227,6 +274,12 @@ export class HaListVirtualized extends HaListBase {
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleVisibilityChanged(ev: { first: number; last: number }) {
|
||||
this._visibleStart = ev.first;
|
||||
this._visibleEnd = ev.last;
|
||||
|
||||
if (!this.virtualFocus) {
|
||||
this.applyActive(false);
|
||||
}
|
||||
fireEvent(this, "ha-list-visibility-changed", {
|
||||
first: ev.first,
|
||||
last: ev.last,
|
||||
|
||||
@@ -30,15 +30,14 @@ import {
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistantConfig } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-check-list-item";
|
||||
import "../ha-dialog";
|
||||
import "../ha-dialog-footer";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-list";
|
||||
import "../ha-spinner";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tip";
|
||||
import "../item/ha-list-item-option";
|
||||
import "../list/ha-list-selectable";
|
||||
import "./ha-media-upload-button";
|
||||
import type { MediaManageDialogParams } from "./show-media-manage-dialog";
|
||||
|
||||
@@ -101,12 +100,6 @@ class DialogMediaManage extends LitElement {
|
||||
}
|
||||
this._filteredChildren =
|
||||
this._currentItem?.children?.filter((child) => !child.can_expand) || [];
|
||||
if (this._filteredChildren.length === 0 && this._selected.size !== 0) {
|
||||
// When running delete all, sometimes the list can throw off a spurious
|
||||
// select event that makes it think that 1 item is still selected. Clear selected
|
||||
// if nothing can be selected.
|
||||
this._selected = new Set();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -218,14 +211,21 @@ class DialogMediaManage extends LitElement {
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
<ha-list multi @selected=${this._handleSelected}>
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label=${this._localize(
|
||||
"ui.components.media-browser.file_management.title"
|
||||
)}
|
||||
@ha-list-item-selected=${this._handleItemSelected}
|
||||
@ha-list-item-deselected=${this._handleItemDeselected}
|
||||
>
|
||||
${repeat(
|
||||
this._filteredChildren,
|
||||
(item) => item.media_content_id,
|
||||
(item) => {
|
||||
const icon = html`
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${
|
||||
MediaClassBrowserSettings[
|
||||
item.media_class === "directory"
|
||||
@@ -237,22 +237,23 @@ class DialogMediaManage extends LitElement {
|
||||
></ha-svg-icon>
|
||||
`;
|
||||
return html`
|
||||
<ha-check-list-item
|
||||
<ha-list-item-option
|
||||
${animate({
|
||||
id: item.media_content_id,
|
||||
skipInitial: true,
|
||||
})}
|
||||
graphic="icon"
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.disabled=${this._uploading || this._deleting}
|
||||
.selected=${this._selected.has(fileIndex++)}
|
||||
.item=${item}
|
||||
>
|
||||
${icon} ${item.title}
|
||||
</ha-check-list-item>
|
||||
${icon}
|
||||
<span slot="headline">${item.title}</span>
|
||||
</ha-list-item-option>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-selectable>
|
||||
`
|
||||
}
|
||||
${
|
||||
@@ -278,8 +279,14 @@ class DialogMediaManage extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSelected(ev) {
|
||||
this._selected = ev.detail.index;
|
||||
private _handleItemSelected(ev: CustomEvent<number>) {
|
||||
this._selected = new Set(this._selected).add(ev.detail);
|
||||
}
|
||||
|
||||
private _handleItemDeselected(ev: CustomEvent<number>) {
|
||||
const selected = new Set(this._selected);
|
||||
selected.delete(ev.detail);
|
||||
this._selected = selected;
|
||||
}
|
||||
|
||||
private _startUploading() {
|
||||
|
||||
+29
-25
@@ -357,35 +357,18 @@ export const computeMediaControls = (
|
||||
|
||||
if (
|
||||
!assumedState &&
|
||||
((state === "playing" &&
|
||||
(supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE) ||
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.STOP))) ||
|
||||
((state === "paused" || state === "idle") &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.PLAY)) ||
|
||||
(state === "on" &&
|
||||
(supportsFeature(stateObj, MediaPlayerEntityFeature.PLAY) ||
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE))))
|
||||
state === "on" &&
|
||||
(supportsFeature(stateObj, MediaPlayerEntityFeature.PLAY) ||
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE))
|
||||
) {
|
||||
buttons.push({
|
||||
icon:
|
||||
state === "on"
|
||||
? mdiPlayPause
|
||||
: state !== "playing"
|
||||
? mdiPlay
|
||||
: supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE)
|
||||
? mdiPause
|
||||
: mdiStop,
|
||||
action:
|
||||
state !== "playing"
|
||||
? "media_play"
|
||||
: supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE)
|
||||
? "media_pause"
|
||||
: "media_stop",
|
||||
icon: mdiPlayPause,
|
||||
action: "media_play",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
assumedState &&
|
||||
(assumedState || state === "paused" || state === "idle") &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.PLAY)
|
||||
) {
|
||||
buttons.push({
|
||||
@@ -395,7 +378,7 @@ export const computeMediaControls = (
|
||||
}
|
||||
|
||||
if (
|
||||
assumedState &&
|
||||
(assumedState || state === "playing") &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE)
|
||||
) {
|
||||
buttons.push({
|
||||
@@ -405,7 +388,7 @@ export const computeMediaControls = (
|
||||
}
|
||||
|
||||
if (
|
||||
assumedState &&
|
||||
(assumedState || state === "playing" || state === "paused") &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.STOP)
|
||||
) {
|
||||
buttons.push({
|
||||
@@ -443,6 +426,27 @@ export const computeMediaControls = (
|
||||
return buttons.length > 0 ? buttons : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes stop when play or pause is available, as stop is then only shown as
|
||||
* a secondary control in the more-info dialog. Assumed state players keep
|
||||
* play, pause and stop together as main controls.
|
||||
*/
|
||||
export const removeSecondaryStopControl = (
|
||||
stateObj: MediaPlayerEntity,
|
||||
controls: ControlButton[] | undefined
|
||||
): ControlButton[] | undefined => {
|
||||
if (
|
||||
!controls ||
|
||||
stateObj.attributes.assumed_state === true ||
|
||||
!controls.some(
|
||||
(c) => c.action === "media_play" || c.action === "media_pause"
|
||||
)
|
||||
) {
|
||||
return controls;
|
||||
}
|
||||
return controls.filter((c) => c.action !== "media_stop");
|
||||
};
|
||||
|
||||
export const formatMediaTime = (seconds: number | undefined): string => {
|
||||
if (seconds === undefined || seconds === Infinity) {
|
||||
return "";
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
handleMediaControlClick,
|
||||
MediaPlayerEntityFeature,
|
||||
mediaPlayerPlayMedia,
|
||||
removeSecondaryStopControl,
|
||||
} from "../../../data/media-player";
|
||||
import type {
|
||||
HomeAssistantApi,
|
||||
@@ -159,6 +160,9 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
const secondaryTitle = computeMediaDescription(stateObj);
|
||||
const turnOn = controls?.find((c) => c.action === "turn_on");
|
||||
const turnOff = controls?.find((c) => c.action === "turn_off");
|
||||
const mainControls = removeSecondaryStopControl(stateObj, controls);
|
||||
const stop = controls?.find((c) => c.action === "media_stop");
|
||||
const secondaryStop = stop && !mainControls?.includes(stop);
|
||||
|
||||
return html`
|
||||
${
|
||||
@@ -259,7 +263,9 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
"media_play",
|
||||
"media_stop",
|
||||
].map((action) => {
|
||||
const control = controls.find((c) => c.action === action);
|
||||
const control = mainControls?.find(
|
||||
(c) => c.action === action
|
||||
);
|
||||
return control
|
||||
? html`<ha-button
|
||||
variant="brand"
|
||||
@@ -297,6 +303,17 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
}
|
||||
${this._renderVolumeControl()}
|
||||
<div class="controls-row">
|
||||
${
|
||||
secondaryStop
|
||||
? this._renderControlButton(
|
||||
"media_stop",
|
||||
this._localize("ui.card.media_player.media_stop"),
|
||||
stop.icon,
|
||||
this._handleClick,
|
||||
stop.action
|
||||
)
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
|
||||
? this._renderControlButton(
|
||||
|
||||
@@ -1064,6 +1064,9 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.search-toolbar ha-input-search {
|
||||
min-width: 0;
|
||||
}
|
||||
.filters {
|
||||
--mdc-text-field-fill-color: var(--input-fill-color);
|
||||
--mdc-text-field-idle-line-color: var(--input-idle-line-color);
|
||||
|
||||
@@ -5,20 +5,22 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { arrayWith, arrayWithout } from "../../common/array/array-with";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-dropdown";
|
||||
import "../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-list";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/ha-state-icon";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-two-pane-top-app-bar-fixed";
|
||||
import "../../components/item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "../../components/item/ha-list-item-option";
|
||||
import "../../components/list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "../../components/list/ha-list-selectable";
|
||||
import type { Calendar } from "../../data/calendar";
|
||||
import { getCalendars } from "../../data/calendar";
|
||||
import { CalendarEventsController } from "../../data/calendar-events-controller";
|
||||
@@ -177,21 +179,50 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
></ha-icon-button>
|
||||
${
|
||||
showPane
|
||||
? html`<ha-list slot="pane" multi>${calendarItems}</ha-list>${
|
||||
? html`<ha-list-selectable
|
||||
slot="pane"
|
||||
multi
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.calendar.my_calendars"
|
||||
)}
|
||||
@ha-list-item-selected=${this._handleCalendarToggled}
|
||||
@ha-list-item-deselected=${this._handleCalendarToggled}
|
||||
>
|
||||
${this._calendars.map(
|
||||
(selCal) => html`
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${selCal.entity_id}
|
||||
.selected=${!this._deSelectedCalendars.includes(
|
||||
selCal.entity_id
|
||||
)}
|
||||
>
|
||||
<ha-state-icon
|
||||
slot="start"
|
||||
.stateObj=${selCal}
|
||||
style="--icon-primary-color: ${selCal.backgroundColor}"
|
||||
></ha-state-icon>
|
||||
<span slot="headline">${selCal.name}</span>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
${
|
||||
this.hass.user?.is_admin
|
||||
? html`<ha-list-item
|
||||
graphic="icon"
|
||||
? html`<ha-button
|
||||
slot="pane-footer"
|
||||
appearance="filled"
|
||||
@click=${this._addCalendar}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlus}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.components.calendar.create_calendar"
|
||||
)}
|
||||
</ha-list-item>`
|
||||
</ha-button>`
|
||||
: nothing
|
||||
}`
|
||||
: nothing
|
||||
@@ -227,12 +258,26 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
private _requestSelected(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const item = ev.currentTarget as HaDropdownItem;
|
||||
const entityId = item.value as string;
|
||||
const checked = item.checked;
|
||||
this._setCalendarSelected(item.value as string, !item.checked);
|
||||
}
|
||||
|
||||
if (!checked) {
|
||||
this._deSelectedCalendars = this._deSelectedCalendars.filter(
|
||||
(cal) => cal !== entityId
|
||||
private _handleCalendarToggled(ev: CustomEvent<number>) {
|
||||
const option = (ev.currentTarget as HaListSelectable).items[ev.detail] as
|
||||
HaListItemOption | undefined;
|
||||
if (!option?.value) {
|
||||
return;
|
||||
}
|
||||
this._setCalendarSelected(
|
||||
option.value,
|
||||
ev.type === "ha-list-item-selected"
|
||||
);
|
||||
}
|
||||
|
||||
private _setCalendarSelected(entityId: string, selected: boolean) {
|
||||
if (selected) {
|
||||
this._deSelectedCalendars = arrayWithout(
|
||||
this._deSelectedCalendars,
|
||||
entityId
|
||||
);
|
||||
const calendar = this._calendars.find(
|
||||
(cal) => cal.entity_id === entityId
|
||||
@@ -242,7 +287,10 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
this._subscribeCalendarEvents([calendar]);
|
||||
} else {
|
||||
this._deSelectedCalendars = [...this._deSelectedCalendars, entityId];
|
||||
this._deSelectedCalendars = arrayWith(
|
||||
this._deSelectedCalendars,
|
||||
entityId
|
||||
);
|
||||
this._events.unsubscribe(entityId);
|
||||
}
|
||||
}
|
||||
@@ -311,14 +359,25 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
ha-dropdown ha-button {
|
||||
--ha-font-size-m: var(--ha-font-size-l);
|
||||
}
|
||||
ha-button[slot="pane-footer"] {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin: var(--ha-space-2) auto var(--ha-space-2);
|
||||
}
|
||||
|
||||
ha-dropdown-item {
|
||||
ha-dropdown-item[type="checkbox"] {
|
||||
padding-left: 32px;
|
||||
padding-inline-start: 32px;
|
||||
padding-inline-end: initial;
|
||||
--icon-primary-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
}
|
||||
|
||||
/* The dropdown indents every item when some are checkboxes, but the
|
||||
create item sits below a divider */
|
||||
ha-dropdown-item:not([type="checkbox"]) {
|
||||
padding-inline-start: 1em;
|
||||
}
|
||||
|
||||
ha-dropdown-item[aria-checked="true"] {
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
|
||||
@@ -942,6 +942,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
mdiShareVariant,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { DataTableSettingsController } from "../../../common/controllers/data-table-settings-controller";
|
||||
@@ -27,6 +27,7 @@ import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import { hasRejectedItems } from "../../../common/util/promise-all-settled-results";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
DataTableColumnData,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
@@ -191,30 +192,16 @@ class HaBlueprintOverview extends LitElement {
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc
|
||||
): DataTableColumnContainer<BlueprintMetaDataPath> => ({
|
||||
name: {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
},
|
||||
translated_type: {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.type"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
urlName: "type",
|
||||
},
|
||||
path: {
|
||||
localize: LocalizeFunc,
|
||||
narrow: boolean
|
||||
): DataTableColumnContainer<BlueprintMetaDataPath> => {
|
||||
const path: DataTableColumnData<BlueprintMetaDataPath> = {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.file_name"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
flex: 2,
|
||||
},
|
||||
usage_count: {
|
||||
};
|
||||
const usageCount: DataTableColumnData<BlueprintMetaDataPath> = {
|
||||
title: localize(
|
||||
"ui.panel.config.blueprint.overview.headers.usage_count"
|
||||
),
|
||||
@@ -225,6 +212,14 @@ class HaBlueprintOverview extends LitElement {
|
||||
maxWidth: "90px",
|
||||
template: (blueprint) => {
|
||||
const count = blueprint.usageCount ?? 0;
|
||||
if (narrow) {
|
||||
if (blueprint.error) {
|
||||
return nothing;
|
||||
}
|
||||
return localize("ui.panel.config.blueprint.overview.used_count", {
|
||||
count,
|
||||
});
|
||||
}
|
||||
return html`
|
||||
<ha-assist-chip
|
||||
filled
|
||||
@@ -243,78 +238,98 @@ class HaBlueprintOverview extends LitElement {
|
||||
></ha-assist-chip>
|
||||
`;
|
||||
},
|
||||
},
|
||||
fullpath: {
|
||||
title: "fullpath",
|
||||
hidden: true,
|
||||
},
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
title: "",
|
||||
label: this.hass.localize("ui.panel.config.generic.headers.actions"),
|
||||
type: "overflow-menu",
|
||||
showNarrow: true,
|
||||
template: (blueprint) =>
|
||||
blueprint.error
|
||||
? html`<ha-svg-icon
|
||||
style="color: var(--error-color); display: block; margin-inline-end: 12px; margin-inline-start: auto;"
|
||||
.path=${mdiAlertCircle}
|
||||
></ha-svg-icon>`
|
||||
: html`
|
||||
<ha-icon-overflow-menu
|
||||
narrow
|
||||
.items=${[
|
||||
{
|
||||
path: mdiPlus,
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.blueprint.overview.create_${blueprint.type}`
|
||||
),
|
||||
action: () => this._createNew(blueprint),
|
||||
},
|
||||
{
|
||||
path: mdiEye,
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.blueprint.overview.view_${blueprint.domain}`
|
||||
),
|
||||
action: () => this._showUsed(blueprint),
|
||||
},
|
||||
{
|
||||
path: mdiShareVariant,
|
||||
disabled: !blueprint.source_url,
|
||||
label: this.hass.localize(
|
||||
blueprint.source_url
|
||||
? "ui.panel.config.blueprint.overview.share_blueprint"
|
||||
: "ui.panel.config.blueprint.overview.share_blueprint_no_url"
|
||||
),
|
||||
action: () => this._share(blueprint),
|
||||
},
|
||||
{
|
||||
path: mdiDownload,
|
||||
disabled: !blueprint.source_url,
|
||||
label: this.hass.localize(
|
||||
blueprint.source_url
|
||||
? "ui.panel.config.blueprint.overview.re_import_blueprint"
|
||||
: "ui.panel.config.blueprint.overview.re_import_blueprint_no_url"
|
||||
),
|
||||
action: () => this._reImport(blueprint),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.blueprint.overview.delete_blueprint"
|
||||
),
|
||||
path: mdiDelete,
|
||||
action: () => this._delete(blueprint),
|
||||
warning: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
</ha-icon-overflow-menu>
|
||||
`,
|
||||
},
|
||||
})
|
||||
};
|
||||
return {
|
||||
name: {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
},
|
||||
translated_type: {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.type"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
urlName: "type",
|
||||
},
|
||||
...(narrow
|
||||
? { usage_count: usageCount, path }
|
||||
: { path, usage_count: usageCount }),
|
||||
fullpath: {
|
||||
title: "fullpath",
|
||||
hidden: true,
|
||||
},
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
title: "",
|
||||
label: this.hass.localize("ui.panel.config.generic.headers.actions"),
|
||||
type: "overflow-menu",
|
||||
showNarrow: true,
|
||||
template: (blueprint) =>
|
||||
blueprint.error
|
||||
? html`<ha-svg-icon
|
||||
style="color: var(--error-color); display: block; margin-inline-end: 12px; margin-inline-start: auto;"
|
||||
.path=${mdiAlertCircle}
|
||||
></ha-svg-icon>`
|
||||
: html`
|
||||
<ha-icon-overflow-menu
|
||||
narrow
|
||||
.items=${[
|
||||
{
|
||||
path: mdiPlus,
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.blueprint.overview.create_${blueprint.type}`
|
||||
),
|
||||
action: () => this._createNew(blueprint),
|
||||
},
|
||||
{
|
||||
path: mdiEye,
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.blueprint.overview.view_${blueprint.domain}`
|
||||
),
|
||||
action: () => this._showUsed(blueprint),
|
||||
},
|
||||
{
|
||||
path: mdiShareVariant,
|
||||
disabled: !blueprint.source_url,
|
||||
label: this.hass.localize(
|
||||
blueprint.source_url
|
||||
? "ui.panel.config.blueprint.overview.share_blueprint"
|
||||
: "ui.panel.config.blueprint.overview.share_blueprint_no_url"
|
||||
),
|
||||
action: () => this._share(blueprint),
|
||||
},
|
||||
{
|
||||
path: mdiDownload,
|
||||
disabled: !blueprint.source_url,
|
||||
label: this.hass.localize(
|
||||
blueprint.source_url
|
||||
? "ui.panel.config.blueprint.overview.re_import_blueprint"
|
||||
: "ui.panel.config.blueprint.overview.re_import_blueprint_no_url"
|
||||
),
|
||||
action: () => this._reImport(blueprint),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.blueprint.overview.delete_blueprint"
|
||||
),
|
||||
path: mdiDelete,
|
||||
action: () => this._delete(blueprint),
|
||||
warning: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
</ha-icon-overflow-menu>
|
||||
`,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -373,7 +388,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.columns=${this._columns(this.hass.localize, this.narrow)}
|
||||
.data=${this._processedBlueprints(
|
||||
this.blueprints,
|
||||
this.hass.localize,
|
||||
|
||||
@@ -1645,6 +1645,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
|
||||
@@ -10,12 +10,12 @@ import type {
|
||||
} from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button-next";
|
||||
import "../../../../../components/ha-list";
|
||||
import "../../../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../../../components/input/ha-input-search";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
|
||||
import "../../../../../components/list/ha-list-base";
|
||||
import "../../../../../components/list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "../../../../../components/list/ha-list-selectable";
|
||||
import {
|
||||
@@ -124,14 +124,14 @@ export class ZHADeviceEndpointList extends LitElement {
|
||||
</ha-list-selectable>
|
||||
`
|
||||
: html`
|
||||
<ha-list>
|
||||
<ha-list-base>
|
||||
${repeat(
|
||||
deviceEndpoints,
|
||||
(deviceEndpoint) => deviceEndpoint.id,
|
||||
(deviceEndpoint) =>
|
||||
this._renderReadonlyListRow(deviceEndpoint)
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-base>
|
||||
`
|
||||
}
|
||||
`
|
||||
@@ -369,7 +369,7 @@ export class ZHADeviceEndpointList extends LitElement {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
}
|
||||
|
||||
ha-list,
|
||||
ha-list-base,
|
||||
ha-list-selectable {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -377,11 +377,12 @@ export class ZHADeviceEndpointList extends LitElement {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ha-list-base::part(base),
|
||||
ha-list-selectable::part(base) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-card.scrollable ha-list,
|
||||
ha-card.scrollable ha-list-base,
|
||||
ha-card.scrollable ha-list-selectable {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -17,15 +17,12 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { atLeastVersion } from "../../../common/config/version";
|
||||
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type {
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../../common/dom/fire_event";
|
||||
import {
|
||||
getHistoryState,
|
||||
navigate,
|
||||
updateHistoryState,
|
||||
} from "../../../common/navigate";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import { currentPath } from "../../../common/url/current-path";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
@@ -35,6 +32,7 @@ import {
|
||||
logsLinkFromUrl,
|
||||
logsQueryParamConfig,
|
||||
logsQueryParams,
|
||||
type LogsState,
|
||||
} from "../../../common/url/logs-query-params";
|
||||
import { createQueryString } from "../../../common/url/query-params";
|
||||
import "../../../components/ha-app-icon";
|
||||
@@ -93,8 +91,6 @@ const logProviders: LogProvider[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const LOGS_HISTORY_STATE = "logsPage";
|
||||
|
||||
interface LogProviderPickerItem extends PickerComboBoxItem {
|
||||
addon?: HassioAddonInfo;
|
||||
hasAppIcon?: boolean;
|
||||
@@ -128,6 +124,27 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
@state() private _integrationsExpanded = true;
|
||||
|
||||
// What the user picked, shown again when the page is opened without params,
|
||||
// like from the sidebar. What a link shows isn't stored.
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "logs-search",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _storedSearch = "";
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "logs-integrations",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _storedIntegrations: string[] = [];
|
||||
|
||||
@storage({ key: "logs-raw", state: false, subscribe: false })
|
||||
private _storedRaw = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: manifestsContext, subscribe: true })
|
||||
private _manifests?: ContextType<typeof manifestsContext>;
|
||||
@@ -138,7 +155,7 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
private _queryParams = new QueryParamsController(this, logsQueryParamConfig, {
|
||||
enabled: () => true,
|
||||
navigated: () => this._applyUrl(),
|
||||
navigated: (ev) => this._navigated(ev),
|
||||
});
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -164,6 +181,10 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
private _filterChanged(ev: HASSDomTargetEvent<HaInputSearch>) {
|
||||
this._filter = ev.target.value ?? "";
|
||||
|
||||
if (this._currentProvider() === DEFAULT_LOG_PROVIDER) {
|
||||
this._storedSearch = this._filter;
|
||||
}
|
||||
this._writeUrl(true);
|
||||
}
|
||||
|
||||
@@ -301,6 +322,10 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
private _showDetail() {
|
||||
this._detail = !this._detail;
|
||||
|
||||
if (this._currentProvider() === DEFAULT_LOG_PROVIDER) {
|
||||
this._storedRaw = this._detail;
|
||||
}
|
||||
this._writeUrl();
|
||||
}
|
||||
|
||||
@@ -317,12 +342,12 @@ export class HaConfigLogs extends LitElement {
|
||||
this._selectedLogProvider = provider;
|
||||
this._filter = "";
|
||||
this._integrations = [];
|
||||
this._storedSearch = "";
|
||||
this._storedIntegrations = [];
|
||||
// Keep the search typed so far on the entry Back returns to
|
||||
this._queryParams.flush();
|
||||
// Picking another provider is a navigation of its own, so Back returns
|
||||
// to the previous one. Both entries are marked, so going back to one
|
||||
// without params shows the default view instead of keeping the provider.
|
||||
updateHistoryState({ [LOGS_HISTORY_STATE]: true });
|
||||
// to the previous one
|
||||
const search = createQueryString(
|
||||
logsQueryParams({
|
||||
provider,
|
||||
@@ -332,10 +357,9 @@ export class HaConfigLogs extends LitElement {
|
||||
}),
|
||||
logsQueryParamConfig
|
||||
);
|
||||
|
||||
// From the current path, so a trailing slash still matches the controller
|
||||
navigate(`${currentPath()}${search ? `?${search}` : ""}`, {
|
||||
data: { [LOGS_HISTORY_STATE]: true },
|
||||
});
|
||||
navigate(`${currentPath()}${search ? `?${search}` : ""}`);
|
||||
}
|
||||
|
||||
private _toggleFilters() {
|
||||
@@ -351,12 +375,27 @@ export class HaConfigLogs extends LitElement {
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
this._integrations = Array.isArray(value) ? value : [];
|
||||
const integrations = Array.isArray(value) ? value : [];
|
||||
|
||||
// The filter also sends the value it was given once it has its items, in
|
||||
// its own order, so only a change is the user's
|
||||
if (
|
||||
this._currentProvider() === DEFAULT_LOG_PROVIDER &&
|
||||
!deepEqual([...integrations].sort(), [...this._integrations].sort())
|
||||
) {
|
||||
this._storedIntegrations = integrations;
|
||||
}
|
||||
|
||||
this._integrations = integrations;
|
||||
this._writeUrl();
|
||||
}
|
||||
|
||||
private _clearIntegrations() {
|
||||
this._integrations = [];
|
||||
|
||||
if (this._currentProvider() === DEFAULT_LOG_PROVIDER) {
|
||||
this._storedIntegrations = [];
|
||||
}
|
||||
this._writeUrl();
|
||||
}
|
||||
|
||||
@@ -366,20 +405,49 @@ export class HaConfigLogs extends LitElement {
|
||||
this._integrationsExpanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
// Until the apps are loaded, the provider is the one the URL asks for
|
||||
private _currentProvider(): string {
|
||||
return this._providersLoaded
|
||||
? this._selectedLogProvider
|
||||
: (logsLinkFromUrl(this._queryParams.searchParams() ?? "")?.provider ??
|
||||
DEFAULT_LOG_PROVIDER);
|
||||
}
|
||||
|
||||
private _writeUrl(debounced = false) {
|
||||
this._queryParams.write(
|
||||
logsQueryParams({
|
||||
// Until the apps are loaded, keep the provider the URL asks for
|
||||
provider: this._providersLoaded
|
||||
? this._selectedLogProvider
|
||||
: (logsLinkFromUrl(this._queryParams.searchParams() ?? "")
|
||||
?.provider ?? DEFAULT_LOG_PROVIDER),
|
||||
search: this._filter,
|
||||
integrations: this._integrations,
|
||||
raw: this._detail,
|
||||
}),
|
||||
debounced
|
||||
);
|
||||
this._queryParams.write(logsQueryParams(this._urlState()), debounced);
|
||||
}
|
||||
|
||||
private _urlState(): LogsState {
|
||||
return {
|
||||
provider: this._currentProvider(),
|
||||
search: this._filter,
|
||||
integrations: this._integrations,
|
||||
raw: this._detail,
|
||||
};
|
||||
}
|
||||
|
||||
// Closing a dialog, like an entry's details, goes back to the URL the page
|
||||
// is already showing, so there's nothing to apply. A link is always applied.
|
||||
private _navigated(ev?: Event) {
|
||||
const searchParams = this._queryParams.searchParams();
|
||||
|
||||
if (ev?.type === "popstate" && searchParams) {
|
||||
const shown = createQueryString(
|
||||
logsQueryParams(this._urlState()),
|
||||
logsQueryParamConfig
|
||||
);
|
||||
|
||||
const back = createQueryString(
|
||||
logsQueryParams(logsLinkFromUrl(searchParams) ?? DEFAULT_LOGS_STATE),
|
||||
logsQueryParamConfig
|
||||
);
|
||||
|
||||
if (back === shown) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._applyUrl();
|
||||
}
|
||||
|
||||
private async _init() {
|
||||
@@ -389,18 +457,30 @@ export class HaConfigLogs extends LitElement {
|
||||
// Keep a search typed while the apps were loading
|
||||
this._queryParams.flush();
|
||||
this._providersLoaded = true;
|
||||
this._applyUrl();
|
||||
// What was stored is already applied, and an empty URL now is a link like
|
||||
// `?provider=core` that was tidied up
|
||||
this._applyUrl(false);
|
||||
}
|
||||
|
||||
private _applyUrl() {
|
||||
private _applyUrl(restoreStored = true) {
|
||||
const searchParams = this._queryParams.searchParams();
|
||||
if (!searchParams) {
|
||||
return;
|
||||
}
|
||||
// A link is shown, but not stored. Opened without params, like from the
|
||||
// sidebar or Back to before another provider was picked, the page shows
|
||||
// what the user picked.
|
||||
const link =
|
||||
logsLinkFromUrl(searchParams) ??
|
||||
(getHistoryState()?.[LOGS_HISTORY_STATE]
|
||||
? DEFAULT_LOGS_STATE
|
||||
(searchParams.toString() === ""
|
||||
? restoreStored
|
||||
? {
|
||||
...DEFAULT_LOGS_STATE,
|
||||
search: this._storedSearch,
|
||||
integrations: this._storedIntegrations,
|
||||
raw: this._storedRaw,
|
||||
}
|
||||
: DEFAULT_LOGS_STATE
|
||||
: undefined);
|
||||
let missingProvider: string | undefined;
|
||||
if (link) {
|
||||
|
||||
@@ -106,6 +106,10 @@ class HaConfigSectionNetwork extends LitElement {
|
||||
margin-bottom: 24px;
|
||||
max-width: 600px;
|
||||
}
|
||||
.discovery-card ha-list-item-button::part(headline),
|
||||
.discovery-card ha-list-item-button::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/item/ha-list-item-base";
|
||||
import "../../../../components/ha-switch";
|
||||
@@ -43,6 +43,13 @@ class HaDebugConnectionRow extends LitElement {
|
||||
setDebugConnection(debugConnection);
|
||||
storeState(this.hass);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { setViewTransitionDisabled } from "../../../../common/util/view-transition";
|
||||
@@ -41,6 +41,13 @@ class HaDebugDisableViewTransitionRow extends LitElement {
|
||||
this._disabled = (ev.target as HaSwitch).checked;
|
||||
setViewTransitionDisabled(this._disabled);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
@@ -42,6 +42,13 @@ class HaDebugSafeAreaToolRow extends LitElement {
|
||||
private _checkedChanged(ev: Event) {
|
||||
this._enabled = (ev.target as HaSwitch).checked;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "@lit-labs/virtualizer";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
@@ -6,6 +5,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { arrayWith, arrayWithout } from "../../../common/array/array-with";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
|
||||
import {
|
||||
@@ -13,13 +13,14 @@ import {
|
||||
computeEntitySearchLabels,
|
||||
} from "../../../common/entity/compute_entity_name_display";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../components/input/ha-input-search";
|
||||
import "../../../components/item/ha-list-item-option";
|
||||
import "../../../components/list/ha-list-selectable-virtualized";
|
||||
import type { HaListVirtualizedItem } from "../../../components/list/ha-list-virtualized";
|
||||
import {
|
||||
configContext,
|
||||
internationalizationContext,
|
||||
@@ -30,10 +31,13 @@ import type { ExposeEntitySettings } from "../../../data/expose";
|
||||
import { voiceAssistants } from "../../../data/expose";
|
||||
import { DialogMixin } from "../../../dialogs/dialog-mixin";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle, haStyleScrollbar } from "../../../resources/styles";
|
||||
import { loadVirtualizer } from "../../../resources/virtualizer";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { ExposeEntityDialogParams } from "./show-dialog-expose-entity";
|
||||
|
||||
interface ExposeEntityRow extends HaListVirtualizedItem {
|
||||
stateObj: HassEntity;
|
||||
}
|
||||
|
||||
@customElement("dialog-expose-entity")
|
||||
class DialogExposeEntity extends DirtyStateProviderMixin<string[]>()(
|
||||
DialogMixin<ExposeEntityDialogParams>(LitElement)
|
||||
@@ -93,29 +97,25 @@ class DialogExposeEntity extends DirtyStateProviderMixin<string[]>()(
|
||||
header-title=${header}
|
||||
header-subtitle=${subtitle}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@after-show=${this._loadVirtualizer}
|
||||
@after-show=${this._dialogShown}
|
||||
>
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._filterChanged}
|
||||
></ha-input-search>
|
||||
<ha-list multi>
|
||||
${
|
||||
this._dialogReady
|
||||
? html` <lit-virtualizer
|
||||
scroller
|
||||
class="ha-scrollbar"
|
||||
@click=${this._itemClicked}
|
||||
@keydown=${this._handleItemKeydown}
|
||||
.items=${entities}
|
||||
.renderItem=${this._renderItem}
|
||||
.keyFunction=${this._keyFunction}
|
||||
>
|
||||
</lit-virtualizer>`
|
||||
: nothing
|
||||
}
|
||||
</ha-list>
|
||||
${
|
||||
this._dialogReady
|
||||
? html`<ha-list-selectable-virtualized
|
||||
multi
|
||||
aria-label=${header}
|
||||
.rows=${entities}
|
||||
.rowRenderer=${this._renderItem}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
></ha-list-selectable-virtualized>`
|
||||
: nothing
|
||||
}
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@@ -139,36 +139,25 @@ class DialogExposeEntity extends DirtyStateProviderMixin<string[]>()(
|
||||
`;
|
||||
}
|
||||
|
||||
private async _loadVirtualizer() {
|
||||
await loadVirtualizer();
|
||||
private _dialogShown() {
|
||||
this._dialogReady = true;
|
||||
}
|
||||
|
||||
private _keyFunction = (entity: HassEntity) => entity.entity_id;
|
||||
|
||||
private _handleSelected = (ev) => {
|
||||
const entityId = ev.target.value;
|
||||
if (ev.detail.selected) {
|
||||
if (this._selected.includes(entityId)) {
|
||||
return;
|
||||
}
|
||||
this._selected = [...this._selected, entityId];
|
||||
} else {
|
||||
this._selected = this._selected.filter((item) => item !== entityId);
|
||||
private _handleItemToggled(ev: CustomEvent<number>) {
|
||||
const entityId = this._filterEntities(
|
||||
this.params!.exposedEntities,
|
||||
this._registries,
|
||||
this._filter
|
||||
)[ev.detail]?.id;
|
||||
if (!entityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._selected =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this._selected, entityId)
|
||||
: arrayWithout(this._selected, entityId);
|
||||
this._updateDirtyState(this._selected);
|
||||
};
|
||||
|
||||
private _handleItemKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
this._itemClicked(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _itemClicked(ev) {
|
||||
const listItem = ev.target.closest("ha-check-list-item");
|
||||
listItem.selected = !listItem.selected;
|
||||
}
|
||||
|
||||
private _filterChanged(e: HASSDomTargetEvent<HaInputSearch>) {
|
||||
@@ -180,9 +169,9 @@ class DialogExposeEntity extends DirtyStateProviderMixin<string[]>()(
|
||||
exposedEntities: Record<string, ExposeEntitySettings>,
|
||||
registries: ContextType<typeof registriesContext>,
|
||||
filter?: string
|
||||
): HassEntity[] => {
|
||||
): ExposeEntityRow[] => {
|
||||
const lowerFilter = filter?.toLowerCase();
|
||||
const result: HassEntity[] = [];
|
||||
const result: ExposeEntityRow[] = [];
|
||||
|
||||
for (const entity of Object.values(this._states)) {
|
||||
if (
|
||||
@@ -197,7 +186,11 @@ class DialogExposeEntity extends DirtyStateProviderMixin<string[]>()(
|
||||
!lowerFilter ||
|
||||
entity.entity_id.toLowerCase().includes(lowerFilter)
|
||||
) {
|
||||
result.push(entity);
|
||||
result.push({
|
||||
id: entity.entity_id,
|
||||
interactive: true,
|
||||
stateObj: entity,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -209,7 +202,11 @@ class DialogExposeEntity extends DirtyStateProviderMixin<string[]>()(
|
||||
name?.toLowerCase().includes(lowerFilter)
|
||||
)
|
||||
) {
|
||||
result.push(entity);
|
||||
result.push({
|
||||
id: entity.entity_id,
|
||||
interactive: true,
|
||||
stateObj: entity,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +214,8 @@ class DialogExposeEntity extends DirtyStateProviderMixin<string[]>()(
|
||||
}
|
||||
);
|
||||
|
||||
private _renderItem = (entityState: HassEntity) => {
|
||||
private _renderItem = (row: ExposeEntityRow) => {
|
||||
const entityState = row.stateObj;
|
||||
const { primary, secondary: context } = computeEntityPickerDisplay(
|
||||
{
|
||||
entities: this._registries.entities,
|
||||
@@ -232,36 +230,31 @@ class DialogExposeEntity extends DirtyStateProviderMixin<string[]>()(
|
||||
const showEntityId = this._config?.userData?.showEntityIdPicker;
|
||||
|
||||
return html`
|
||||
<ha-check-list-item
|
||||
tabindex="0"
|
||||
graphic="icon"
|
||||
?twoLine=${context}
|
||||
?threeLine=${showEntityId}
|
||||
.value=${entityState.entity_id}
|
||||
.selected=${this._selected.includes(entityState.entity_id)}
|
||||
@request-selected=${this._handleSelected}
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${row.id}
|
||||
.selected=${this._selected.includes(row.id)}
|
||||
>
|
||||
<ha-state-icon
|
||||
title=${ifDefined(entityState?.state)}
|
||||
slot="graphic"
|
||||
title=${ifDefined(entityState.state)}
|
||||
slot="start"
|
||||
.stateObj=${entityState}
|
||||
></ha-state-icon>
|
||||
${primary}
|
||||
<span slot="headline">${primary}</span>
|
||||
${
|
||||
context || showEntityId
|
||||
? html`<span slot="secondary">
|
||||
? html`<span slot="supporting-text">
|
||||
${context}
|
||||
${
|
||||
showEntityId
|
||||
? html`<br /><span class="entity-id"
|
||||
>${entityState.entity_id}</span
|
||||
>`
|
||||
? html`${context ? html`<br />` : nothing}${row.id}`
|
||||
: nothing
|
||||
}
|
||||
</span>`
|
||||
: nothing
|
||||
}
|
||||
</ha-check-list-item>
|
||||
</ha-list-item-option>
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -274,53 +267,28 @@ class DialogExposeEntity extends DirtyStateProviderMixin<string[]>()(
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
ha-list {
|
||||
position: relative;
|
||||
}
|
||||
lit-virtualizer {
|
||||
height: 500px;
|
||||
}
|
||||
ha-input-search {
|
||||
padding: 0 var(--ha-space-3);
|
||||
}
|
||||
lit-virtualizer {
|
||||
width: 100%;
|
||||
contain: size layout !important;
|
||||
}
|
||||
ha-check-list-item {
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
}
|
||||
ha-check-list-item[threeLine] {
|
||||
height: 88px;
|
||||
}
|
||||
ha-check-list-item .entity-id {
|
||||
line-height: var(--ha-line-height-normal);
|
||||
padding-left: var(--ha-space-1);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
ha-check-list-item ha-state-icon {
|
||||
margin-left: var(--ha-space-6);
|
||||
margin-inline-start: var(--ha-space-6);
|
||||
margin-inline-end: initial;
|
||||
ha-list-selectable-virtualized {
|
||||
height: 500px;
|
||||
}
|
||||
@media all and (max-height: 800px) {
|
||||
lit-virtualizer {
|
||||
ha-list-selectable-virtualized {
|
||||
height: 334px;
|
||||
}
|
||||
}
|
||||
@media all and (max-height: 600px) {
|
||||
lit-virtualizer {
|
||||
ha-list-selectable-virtualized {
|
||||
height: 238px;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 500px), all and (max-height: 500px) {
|
||||
lit-virtualizer {
|
||||
ha-list-selectable-virtualized {
|
||||
height: calc(
|
||||
100vh -
|
||||
210px - var(--safe-area-inset-top, 0px) - var(
|
||||
@@ -329,11 +297,6 @@ class DialogExposeEntity extends DirtyStateProviderMixin<string[]>()(
|
||||
)
|
||||
);
|
||||
}
|
||||
ha-check-list-item ha-state-icon {
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -5,7 +5,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import {
|
||||
consumeEntityState,
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import {
|
||||
ALERT_DOMAINS,
|
||||
DEFAULT_ALERT_COLOR,
|
||||
computeAlertColor,
|
||||
isAlertActive,
|
||||
} from "../../../common/entity/alert_active";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
@@ -121,12 +120,14 @@ export class HuiAlertBadge extends LitElement implements LovelaceBadge {
|
||||
|
||||
const active = isAlertActive(stateObj);
|
||||
|
||||
const color = active
|
||||
? (this._config.alert_color ?? DEFAULT_ALERT_COLOR)
|
||||
: this._config.clear_color;
|
||||
const color = computeAlertColor(
|
||||
stateObj,
|
||||
active
|
||||
? (this._config.alert_color ?? "state")
|
||||
: (this._config.clear_color ?? "none")
|
||||
);
|
||||
|
||||
const pulse = active && this._config.pulse === true;
|
||||
const hasColor = color !== undefined && color !== "none";
|
||||
|
||||
const name = this._formatters.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
@@ -143,10 +144,9 @@ export class HuiAlertBadge extends LitElement implements LovelaceBadge {
|
||||
.type=${hasTileCardAction(this._config) ? "button" : "badge"}
|
||||
.label=${showState && showName ? name : undefined}
|
||||
.iconOnly=${!content}
|
||||
class=${classMap({ alert: true, pulse, "no-color": !hasColor })}
|
||||
class=${classMap({ alert: true, pulse })}
|
||||
style=${styleMap({
|
||||
"--ha-alert-color":
|
||||
color && hasColor ? computeCssColor(color) : undefined,
|
||||
"--ha-alert-color": color,
|
||||
})}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
@@ -205,10 +205,7 @@ export class HuiAlertBadge extends LitElement implements LovelaceBadge {
|
||||
);
|
||||
background: var(--ha-alert-badge-background);
|
||||
--ha-card-background: transparent;
|
||||
--badge-color: var(--ha-alert-color);
|
||||
}
|
||||
ha-badge.alert.no-color {
|
||||
--badge-color: var(--secondary-text-color);
|
||||
--badge-color: var(--ha-alert-color, var(--secondary-text-color));
|
||||
}
|
||||
ha-badge.alert::before {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,21 +1,58 @@
|
||||
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
|
||||
import type { FrontendLocaleData } from "../../../../data/translation";
|
||||
import type { ClockCardConfig, ClockCardDatePart } from "../types";
|
||||
|
||||
type ClockCardSeparatorPart = Extract<
|
||||
import type {
|
||||
ClockCardConfig,
|
||||
ClockCardDatePart,
|
||||
"separator-dash" | "separator-slash" | "separator-dot" | "separator-new-line"
|
||||
>;
|
||||
ClockCardDatePreset,
|
||||
} from "../types";
|
||||
|
||||
type ClockCardSeparatorPart = Extract<ClockCardDatePart, `separator-${string}`>;
|
||||
|
||||
type ClockCardValuePart = Exclude<ClockCardDatePart, ClockCardSeparatorPart>;
|
||||
|
||||
/**
|
||||
* Normalized date configuration used by clock card renderers.
|
||||
* Normalized date configuration used by clock card renderers. A preset wins
|
||||
* over parts.
|
||||
*/
|
||||
interface ClockCardDateConfig {
|
||||
parts: ClockCardDatePart[];
|
||||
preset?: ClockCardDatePreset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Date presets offered before the custom parts editor. `short` to `full`
|
||||
* follow the locale's standard date styles, `iso` is ISO 8601 (YYYY-MM-DD).
|
||||
*/
|
||||
export const CLOCK_CARD_DATE_PRESETS: readonly ClockCardDatePreset[] = [
|
||||
"short",
|
||||
"medium",
|
||||
"long",
|
||||
"full",
|
||||
"iso",
|
||||
];
|
||||
|
||||
const ISO_DATE_OPTIONS: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
};
|
||||
|
||||
const DATE_PRESET_OPTIONS: Record<
|
||||
ClockCardDatePreset,
|
||||
Intl.DateTimeFormatOptions
|
||||
> = {
|
||||
short: { dateStyle: "short" },
|
||||
medium: { dateStyle: "medium" },
|
||||
long: { dateStyle: "long" },
|
||||
full: { dateStyle: "full" },
|
||||
iso: ISO_DATE_OPTIONS,
|
||||
};
|
||||
|
||||
export const isClockCardDatePreset = (
|
||||
value: unknown
|
||||
): value is ClockCardDatePreset =>
|
||||
CLOCK_CARD_DATE_PRESETS.some((preset) => preset === value);
|
||||
|
||||
/**
|
||||
* Resolves the locale and time zone for a clock card from the user's locale,
|
||||
* the server time zone and the card's configuration. Applies the optional
|
||||
@@ -54,6 +91,7 @@ export const CLOCK_CARD_DATE_PARTS: readonly ClockCardDatePart[] = [
|
||||
"separator-dash",
|
||||
"separator-slash",
|
||||
"separator-dot",
|
||||
"separator-comma",
|
||||
"separator-new-line",
|
||||
];
|
||||
|
||||
@@ -77,6 +115,8 @@ const DATE_SEPARATORS: Record<ClockCardSeparatorPart, string> = {
|
||||
"separator-dash": "-",
|
||||
"separator-slash": "/",
|
||||
"separator-dot": ".",
|
||||
// A comma always reads with a space after it, as in "Thursday, 29 October".
|
||||
"separator-comma": ", ",
|
||||
"separator-new-line": "\n",
|
||||
};
|
||||
|
||||
@@ -84,6 +124,7 @@ const DATE_SEPARATOR_PARTS = new Set<ClockCardSeparatorPart>([
|
||||
"separator-dash",
|
||||
"separator-slash",
|
||||
"separator-dot",
|
||||
"separator-comma",
|
||||
"separator-new-line",
|
||||
]);
|
||||
|
||||
@@ -163,7 +204,7 @@ const applyDatePartOption = (
|
||||
* Sanitizes configured date tokens while preserving their literal order.
|
||||
*/
|
||||
const normalizeDateParts = (
|
||||
parts: ClockCardConfig["date_format"]
|
||||
parts: ClockCardDatePart[] | undefined
|
||||
): ClockCardDatePart[] =>
|
||||
parts?.filter((part): part is ClockCardDatePart =>
|
||||
isClockCardDatePart(part)
|
||||
@@ -174,19 +215,31 @@ const normalizeDateParts = (
|
||||
*/
|
||||
export const getClockCardDateConfig = (
|
||||
config?: Pick<ClockCardConfig, "date_format">
|
||||
): ClockCardDateConfig => ({
|
||||
parts: normalizeDateParts(config?.date_format),
|
||||
});
|
||||
): ClockCardDateConfig => {
|
||||
const dateFormat = config?.date_format;
|
||||
|
||||
if (isClockCardDatePreset(dateFormat)) {
|
||||
return { parts: [], preset: dateFormat };
|
||||
}
|
||||
|
||||
return {
|
||||
parts: normalizeDateParts(Array.isArray(dateFormat) ? dateFormat : []),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether the clock configuration resolves to any visible date output.
|
||||
*/
|
||||
export const hasClockCardDate = (
|
||||
config?: Pick<ClockCardConfig, "date_format">
|
||||
): boolean => getClockCardDateConfig(config).parts.length > 0;
|
||||
): boolean => {
|
||||
const dateConfig = getClockCardDateConfig(config);
|
||||
|
||||
return !!dateConfig.preset || dateConfig.parts.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts normalized date tokens into Intl.DateTimeFormat options.
|
||||
* Converts a normalized date config into Intl.DateTimeFormat options.
|
||||
*
|
||||
* Separator tokens are ignored. If multiple tokens target the same Intl field,
|
||||
* the last one wins.
|
||||
@@ -194,6 +247,10 @@ export const hasClockCardDate = (
|
||||
export const getClockCardDateTimeFormatOptions = (
|
||||
dateConfig: ClockCardDateConfig
|
||||
): Intl.DateTimeFormatOptions => {
|
||||
if (dateConfig.preset) {
|
||||
return { ...DATE_PRESET_OPTIONS[dateConfig.preset] };
|
||||
}
|
||||
|
||||
const options: Intl.DateTimeFormatOptions = {};
|
||||
|
||||
dateConfig.parts.forEach((part) => {
|
||||
@@ -203,6 +260,215 @@ export const getClockCardDateTimeFormatOptions = (
|
||||
return options;
|
||||
};
|
||||
|
||||
const DATE_PRESET_FORMATTERS = new Map<string, Intl.DateTimeFormat>();
|
||||
|
||||
const getDatePresetFormatter = (
|
||||
preset: ClockCardDatePreset,
|
||||
language: string,
|
||||
timeZone?: string
|
||||
): Intl.DateTimeFormat => {
|
||||
const cacheKey = `${language}|${timeZone || ""}|${preset}`;
|
||||
const cached = DATE_PRESET_FORMATTERS.get(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// ISO 8601 is fixed: Gregorian calendar and Latin digits in any language.
|
||||
const isIso = preset === "iso";
|
||||
|
||||
const options: Intl.DateTimeFormatOptions = isIso
|
||||
? { ...ISO_DATE_OPTIONS, calendar: "iso8601", numberingSystem: "latn" }
|
||||
: { ...DATE_PRESET_OPTIONS[preset] };
|
||||
|
||||
if (timeZone) {
|
||||
options.timeZone = timeZone;
|
||||
}
|
||||
|
||||
const formatter = new Intl.DateTimeFormat(isIso ? "en" : language, options);
|
||||
|
||||
DATE_PRESET_FORMATTERS.set(cacheKey, formatter);
|
||||
|
||||
return formatter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a date with one of the presets.
|
||||
*/
|
||||
export const formatClockCardDatePreset = (
|
||||
date: Date,
|
||||
preset: ClockCardDatePreset,
|
||||
language: string,
|
||||
timeZone?: string
|
||||
): string => {
|
||||
const formatter = getDatePresetFormatter(preset, language, timeZone);
|
||||
|
||||
if (preset !== "iso") {
|
||||
return formatter.format(date);
|
||||
}
|
||||
|
||||
const values: Partial<Record<Intl.DateTimeFormatPartTypes, string>> = {};
|
||||
|
||||
formatter.formatToParts(date).forEach(({ type, value }) => {
|
||||
values[type] = value;
|
||||
});
|
||||
|
||||
return `${values.year}-${values.month}-${values.day}`;
|
||||
};
|
||||
|
||||
const PRESET_REFERENCE_DATE = new Date(Date.UTC(2026, 0, 5, 12));
|
||||
|
||||
const INLINE_SEPARATOR_PARTS = [
|
||||
"separator-dash",
|
||||
"separator-slash",
|
||||
"separator-dot",
|
||||
"separator-comma",
|
||||
] as const satisfies readonly ClockCardSeparatorPart[];
|
||||
|
||||
// Direction marks Intl adds around separators in right-to-left languages,
|
||||
// such as Arabic.
|
||||
const BIDI_CONTROL_CHARACTERS =
|
||||
/[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
|
||||
|
||||
/**
|
||||
* Approximates a preset as custom date parts in the given language, so
|
||||
* switching from a preset to custom starts from what the user already sees.
|
||||
* Literal text the parts can't express, such as "de" in Spanish, is dropped,
|
||||
* and each part renders in its standalone form.
|
||||
*/
|
||||
export const getClockCardDatePresetParts = (
|
||||
preset: ClockCardDatePreset,
|
||||
language: string
|
||||
): ClockCardDatePart[] => {
|
||||
if (preset === "iso") {
|
||||
return [
|
||||
"year-numeric",
|
||||
"separator-dash",
|
||||
"month-2-digit",
|
||||
"separator-dash",
|
||||
"day-2-digit",
|
||||
];
|
||||
}
|
||||
|
||||
// Compares only the field itself, as standalone formats can add text around
|
||||
// it, such as "05日" for a Japanese day.
|
||||
const formatUtcField = (
|
||||
formatter: Intl.DateTimeFormat,
|
||||
fieldType: Intl.DateTimeFormatPartTypes
|
||||
) =>
|
||||
formatter
|
||||
.formatToParts(PRESET_REFERENCE_DATE)
|
||||
.find(({ type }) => type === fieldType)?.value;
|
||||
|
||||
const formatUtc = (
|
||||
part: ClockCardValuePart,
|
||||
fieldType: Intl.DateTimeFormatPartTypes
|
||||
) => formatUtcField(getDatePartFormatter(part, language, "UTC"), fieldType);
|
||||
|
||||
// Some languages, such as Polish and Russian, inflect the month next to a
|
||||
// day ("5 stycznia"), so it differs from the standalone form ("styczeń").
|
||||
// Only textual months inflect.
|
||||
const formatMonthWithDay = (part: "month-long" | "month-short") =>
|
||||
formatUtcField(
|
||||
new Intl.DateTimeFormat(language, {
|
||||
...DATE_PART_OPTIONS[part],
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
}),
|
||||
"month"
|
||||
);
|
||||
|
||||
// A field that a language formats with its own punctuation, such as "5."
|
||||
// for a Czech day or "2026年" for a Japanese year, already carries the
|
||||
// separator next to it.
|
||||
const hasAffix = (part: ClockCardValuePart, side: "start" | "end") => {
|
||||
const fieldParts = getDatePartFormatter(
|
||||
part,
|
||||
language,
|
||||
"UTC"
|
||||
).formatToParts(PRESET_REFERENCE_DATE);
|
||||
|
||||
return (
|
||||
fieldParts[side === "start" ? 0 : fieldParts.length - 1]?.type ===
|
||||
"literal"
|
||||
);
|
||||
};
|
||||
|
||||
const parts: ClockCardDatePart[] = [];
|
||||
|
||||
getDatePresetFormatter(preset, language, "UTC")
|
||||
.formatToParts(PRESET_REFERENCE_DATE)
|
||||
.forEach(({ type, value }) => {
|
||||
switch (type) {
|
||||
case "weekday":
|
||||
parts.push(
|
||||
value === formatUtc("weekday-long", type)
|
||||
? "weekday-long"
|
||||
: "weekday-short"
|
||||
);
|
||||
break;
|
||||
case "day":
|
||||
parts.push(
|
||||
value === formatUtc("day-2-digit", type)
|
||||
? "day-2-digit"
|
||||
: "day-numeric"
|
||||
);
|
||||
break;
|
||||
case "month":
|
||||
parts.push(
|
||||
(
|
||||
[
|
||||
"month-2-digit",
|
||||
"month-numeric",
|
||||
"month-long",
|
||||
"month-short",
|
||||
] as const
|
||||
).find((part) => value === formatUtc(part, type)) ??
|
||||
(["month-long", "month-short"] as const).find(
|
||||
(part) => value === formatMonthWithDay(part)
|
||||
) ??
|
||||
(/^\p{Nd}+$/u.test(value) ? "month-numeric" : "month-short")
|
||||
);
|
||||
break;
|
||||
case "year":
|
||||
parts.push(
|
||||
value === formatUtc("year-2-digit", type)
|
||||
? "year-2-digit"
|
||||
: "year-numeric"
|
||||
);
|
||||
break;
|
||||
case "literal": {
|
||||
const literal = value.replace(BIDI_CONTROL_CHARACTERS, "");
|
||||
const separator = INLINE_SEPARATOR_PARTS.find(
|
||||
(part) => literal === DATE_SEPARATORS[part]
|
||||
);
|
||||
|
||||
if (separator) {
|
||||
parts.push(separator);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return parts.filter((part, index) => {
|
||||
if (!isDateSeparatorPart(part)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const previous = parts[index - 1];
|
||||
const next = parts[index + 1];
|
||||
|
||||
return !(
|
||||
(previous &&
|
||||
!isDateSeparatorPart(previous) &&
|
||||
hasAffix(previous, "end")) ||
|
||||
(next && !isDateSeparatorPart(next) && hasAffix(next, "start"))
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the final date string from literal date tokens.
|
||||
*
|
||||
@@ -216,6 +482,15 @@ export const formatClockCardDate = (
|
||||
language: string,
|
||||
timeZone?: string
|
||||
): string => {
|
||||
if (dateConfig.preset) {
|
||||
return formatClockCardDatePreset(
|
||||
date,
|
||||
dateConfig.preset,
|
||||
language,
|
||||
timeZone
|
||||
);
|
||||
}
|
||||
|
||||
let result = "";
|
||||
let previousRenderedPartWasValue = false;
|
||||
|
||||
|
||||
@@ -230,6 +230,8 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
const dateConfig = getClockCardDateConfig(config);
|
||||
const showDate = hasClockCardDate(config);
|
||||
const isLongDate =
|
||||
dateConfig.preset === "long" ||
|
||||
dateConfig.preset === "full" ||
|
||||
dateConfig.parts.includes("month-long") ||
|
||||
dateConfig.parts.includes("weekday-long");
|
||||
|
||||
|
||||
@@ -5,12 +5,11 @@ import { customElement, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { consumeEntityState } from "../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import {
|
||||
ALERT_DOMAINS,
|
||||
DEFAULT_ALERT_COLOR,
|
||||
computeAlertColor,
|
||||
isAlertActive,
|
||||
} from "../../../common/entity/alert_active";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
@@ -120,12 +119,14 @@ export class HuiAlertCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const active = isAlertActive(stateObj);
|
||||
|
||||
const color = active
|
||||
? (this._config.alert_color ?? DEFAULT_ALERT_COLOR)
|
||||
: this._config.clear_color;
|
||||
const color = computeAlertColor(
|
||||
stateObj,
|
||||
active
|
||||
? (this._config.alert_color ?? "state")
|
||||
: (this._config.clear_color ?? "none")
|
||||
);
|
||||
|
||||
const pulse = active && this._config.pulse === true;
|
||||
const hasColor = color !== undefined && color !== "none";
|
||||
|
||||
const areaName = this._formatters.formatEntityName(stateObj, {
|
||||
type: "area",
|
||||
@@ -133,10 +134,9 @@ export class HuiAlertCard extends LitElement implements LovelaceCard {
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
class=${classMap({ pulse, "no-color": !hasColor })}
|
||||
class=${classMap({ pulse })}
|
||||
style=${styleMap({
|
||||
"--ha-alert-color":
|
||||
color && hasColor ? computeCssColor(color) : undefined,
|
||||
"--ha-alert-color": color,
|
||||
"--ha-alert-static-opacity": pulse
|
||||
? undefined
|
||||
: "var(--ha-alert-pulse-opacity)",
|
||||
@@ -211,12 +211,7 @@ export class HuiAlertCard extends LitElement implements LovelaceCard {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
ha-card:not(.no-color) {
|
||||
--tile-color: var(--ha-alert-color);
|
||||
}
|
||||
ha-card.no-color {
|
||||
--tile-color: var(--secondary-text-color);
|
||||
--tile-color: var(--ha-alert-color, var(--secondary-text-color));
|
||||
}
|
||||
ha-card::before {
|
||||
position: absolute;
|
||||
|
||||
@@ -62,14 +62,14 @@ export const computeDateText = (
|
||||
config: HassConfig,
|
||||
cardConfig: DateCardConfig
|
||||
): string => {
|
||||
const { parts } = getClockCardDateConfig(cardConfig);
|
||||
const dateConfig = getClockCardDateConfig(cardConfig);
|
||||
const timeZone = computeResolvedTimeZone(locale, config, cardConfig);
|
||||
|
||||
if (!parts.length) {
|
||||
if (!dateConfig.preset && !dateConfig.parts.length) {
|
||||
return formatDefaultDate(dateObj, locale.language, timeZone);
|
||||
}
|
||||
|
||||
return formatClockCardDate(dateObj, { parts }, locale.language, timeZone);
|
||||
return formatClockCardDate(dateObj, dateConfig, locale.language, timeZone);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -310,11 +310,11 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#states > div:first-child > hui-section-row::part(divider) {
|
||||
#states:first-child > div:first-child > hui-section-row::part(divider) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#states > div:first-child > hui-section-row::part(label) {
|
||||
#states:first-child > div:first-child > hui-section-row::part(label) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
handleMediaControlClick,
|
||||
MediaPlayerEntityFeature,
|
||||
mediaPlayerPlayMedia,
|
||||
removeSecondaryStopControl,
|
||||
} from "../../../data/media-player";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import type { Themes } from "../../../data/ws-themes";
|
||||
@@ -213,7 +214,10 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
(isOffState &&
|
||||
!supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON));
|
||||
const hasNoImage = !this._image;
|
||||
const controls = computeMediaControls(stateObj, false);
|
||||
const controls = removeSecondaryStopControl(
|
||||
stateObj,
|
||||
computeMediaControls(stateObj, false)
|
||||
);
|
||||
const showControls =
|
||||
!isUnavailable &&
|
||||
controls &&
|
||||
|
||||
@@ -472,7 +472,7 @@ export interface ClockCardConfig extends LovelaceCardConfig {
|
||||
time_format?: TimeFormat;
|
||||
time_zone?: string;
|
||||
no_background?: boolean;
|
||||
date_format?: ClockCardDatePart[];
|
||||
date_format?: ClockCardDateFormat;
|
||||
// Analog clock options
|
||||
border?: boolean;
|
||||
ticks?: "none" | "quarter" | "hour" | "minute";
|
||||
@@ -493,13 +493,21 @@ export type ClockCardDatePart =
|
||||
| "separator-dash"
|
||||
| "separator-slash"
|
||||
| "separator-dot"
|
||||
| "separator-comma"
|
||||
| "separator-new-line";
|
||||
|
||||
export type ClockCardDatePreset = "short" | "medium" | "long" | "full" | "iso";
|
||||
|
||||
/**
|
||||
* A preset name, or a custom list of date parts in display order.
|
||||
*/
|
||||
export type ClockCardDateFormat = ClockCardDatePreset | ClockCardDatePart[];
|
||||
|
||||
export interface DateCardConfig extends LovelaceCardConfig {
|
||||
type: "date";
|
||||
title?: string;
|
||||
date_size?: "small" | "medium" | "large";
|
||||
date_format?: ClockCardDatePart[];
|
||||
date_format?: ClockCardDateFormat;
|
||||
time_zone?: string;
|
||||
no_background?: boolean;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@ import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { array, assert, literal, object, string } from "superstruct";
|
||||
import {
|
||||
arrayWith,
|
||||
arrayWithout,
|
||||
} from "../../../../../common/array/array-with";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../../../../common/string/compare";
|
||||
import "../../../../../components/ha-check-list-item";
|
||||
import "../../../../../components/ha-list";
|
||||
import "../../../../../components/ha-switch";
|
||||
import "../../../../../components/item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
|
||||
import "../../../../../components/list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "../../../../../components/list/ha-list-selectable";
|
||||
import "../../../../../components/user/ha-user-badge";
|
||||
import type { User } from "../../../../../data/user";
|
||||
import { fetchUsers } from "../../../../../data/user";
|
||||
@@ -57,45 +62,44 @@ export class HaCardConditionUser extends LitElement {
|
||||
const selectedUsers = this.condition.users ?? [];
|
||||
|
||||
return html`
|
||||
<ha-list @selected=${this._handleSelected} multi>
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.condition.user.label"
|
||||
)}
|
||||
@ha-list-item-selected=${this._handleUserToggled}
|
||||
@ha-list-item-deselected=${this._handleUserToggled}
|
||||
>
|
||||
${this._sortedUsers(this._users).map(
|
||||
(user) => html`
|
||||
<ha-check-list-item
|
||||
graphic="avatar"
|
||||
hasMeta
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${user.id}
|
||||
.selected=${selectedUsers.includes(user.id)}
|
||||
>
|
||||
<ha-user-badge slot="graphic" .user=${user}></ha-user-badge>
|
||||
<span>${user.name}</span>
|
||||
</ha-check-list-item>
|
||||
<ha-user-badge slot="start" .user=${user}></ha-user-badge>
|
||||
<span slot="headline">${user.name}</span>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-selectable>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSelected(
|
||||
ev: CustomEvent<{ diff: { added: number[]; removed: number[] } }>
|
||||
) {
|
||||
const sortedUsers = this._sortedUsers(this._users);
|
||||
let users = this.condition.users ?? [];
|
||||
|
||||
if (ev.detail.diff.added.length) {
|
||||
const added = sortedUsers[ev.detail.diff.added[0]].id;
|
||||
if (users.includes(added)) {
|
||||
return;
|
||||
}
|
||||
users = [...users, added];
|
||||
} else if (ev.detail.diff.removed.length) {
|
||||
const removed = sortedUsers[ev.detail.diff.removed[0]].id;
|
||||
if (!users.includes(removed)) {
|
||||
return;
|
||||
}
|
||||
users = users.filter((user) => user !== removed);
|
||||
} else {
|
||||
private _handleUserToggled(ev: CustomEvent<number>) {
|
||||
const option = (ev.currentTarget as HaListSelectable).items[ev.detail] as
|
||||
HaListItemOption | undefined;
|
||||
const userId = option?.value;
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const users =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.condition.users, userId)
|
||||
: arrayWithout(this.condition.users, userId);
|
||||
|
||||
const condition: UserCondition = {
|
||||
...this.condition,
|
||||
users,
|
||||
@@ -108,9 +112,6 @@ export class HaCardConditionUser extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-list {
|
||||
--mdc-list-vertical-padding: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { mdiAlertOutline, mdiTextShort } from "@mdi/js";
|
||||
import { DEFAULT_ALERT_COLOR } from "../../../../common/entity/alert_active";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
import { contentLayoutSchema, displayedElementsSchema } from "./content-schema";
|
||||
@@ -18,7 +17,11 @@ export const alertSchema = () =>
|
||||
{
|
||||
name: "clear_color",
|
||||
selector: {
|
||||
ui_color: { include_none: true, default_color: "none" },
|
||||
ui_color: {
|
||||
include_none: true,
|
||||
include_state: true,
|
||||
default_color: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -26,7 +29,8 @@ export const alertSchema = () =>
|
||||
selector: {
|
||||
ui_color: {
|
||||
include_none: true,
|
||||
default_color: DEFAULT_ALERT_COLOR,
|
||||
include_state: true,
|
||||
default_color: "state",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@@ -28,6 +29,7 @@ import { TimeFormat } from "../../../../data/translation";
|
||||
import { getTimezoneOptions } from "../../../../components/ha-timezone-picker";
|
||||
import {
|
||||
CLOCK_CARD_DATE_PARTS,
|
||||
CLOCK_CARD_DATE_PRESETS,
|
||||
getClockCardDateConfig,
|
||||
} from "../../cards/clock/clock-date-format";
|
||||
|
||||
@@ -41,7 +43,12 @@ const cardConfigStruct = assign(
|
||||
time_zone: optional(enums(getTimezoneOptions().map((option) => option.id))),
|
||||
show_seconds: optional(boolean()),
|
||||
no_background: optional(boolean()),
|
||||
date_format: optional(defaulted(array(enums(CLOCK_CARD_DATE_PARTS)), [])),
|
||||
date_format: optional(
|
||||
union([
|
||||
enums(CLOCK_CARD_DATE_PRESETS),
|
||||
array(enums(CLOCK_CARD_DATE_PARTS)),
|
||||
])
|
||||
),
|
||||
// Analog clock options
|
||||
border: optional(defaulted(boolean(), false)),
|
||||
ticks: optional(
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
assert,
|
||||
assign,
|
||||
boolean,
|
||||
defaulted,
|
||||
enums,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@@ -22,7 +22,10 @@ import type {
|
||||
import type { ValueChangedEvent } from "../../../../types";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { DateCardConfig } from "../../cards/types";
|
||||
import { CLOCK_CARD_DATE_PARTS } from "../../cards/clock/clock-date-format";
|
||||
import {
|
||||
CLOCK_CARD_DATE_PARTS,
|
||||
CLOCK_CARD_DATE_PRESETS,
|
||||
} from "../../cards/clock/clock-date-format";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { getTimezoneOptions } from "../../../../components/ha-timezone-picker";
|
||||
@@ -32,7 +35,12 @@ const cardConfigStruct = assign(
|
||||
object({
|
||||
title: optional(string()),
|
||||
date_size: optional(enums(["small", "medium", "large"] as const)),
|
||||
date_format: optional(defaulted(array(enums(CLOCK_CARD_DATE_PARTS)), [])),
|
||||
date_format: optional(
|
||||
union([
|
||||
enums(CLOCK_CARD_DATE_PRESETS),
|
||||
array(enums(CLOCK_CARD_DATE_PARTS)),
|
||||
])
|
||||
),
|
||||
time_zone: optional(enums(getTimezoneOptions().map((option) => option.id))),
|
||||
no_background: optional(boolean()),
|
||||
})
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { mdiViewDashboard } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-list";
|
||||
import "../../../../components/ha-radio-list-item";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-select";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/item/ha-list-item-option";
|
||||
import "../../../../components/list/ha-list-selectable";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import { fetchConfig } from "../../../../data/lovelace/config/types";
|
||||
import { isStrategyView } from "../../../../data/lovelace/config/view";
|
||||
@@ -156,39 +158,60 @@ export class HuiDialogSelectView extends LitElement {
|
||||
}
|
||||
|
||||
const views = this._selectableConfig.views;
|
||||
if (views.length < 2) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const hasIcon = views.some(({ icon }) => icon);
|
||||
const dashboardTitle = this._params?.allowDashboardChange
|
||||
? this._dashboards.find(
|
||||
(dashboard) =>
|
||||
dashboard.url_path === (this._urlPath ?? LOVELACE_PANEL)
|
||||
)?.title
|
||||
: undefined;
|
||||
const viewsHeading = dashboardTitle
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.editor.select_view.views_heading",
|
||||
{ dashboard: dashboardTitle }
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-list>
|
||||
${viewsHeading ? html`<h3>${viewsHeading}</h3>` : nothing}
|
||||
<ha-list-selectable
|
||||
aria-label=${
|
||||
viewsHeading ??
|
||||
this.hass.localize("ui.panel.lovelace.editor.select_view.views_label")
|
||||
}
|
||||
@ha-list-item-selected=${this._viewChanged}
|
||||
>
|
||||
${views.map((view, idx) => {
|
||||
const isStrategy = isStrategyView(view);
|
||||
|
||||
return html`
|
||||
<ha-radio-list-item
|
||||
.graphic=${hasIcon ? "icon" : nothing}
|
||||
@click=${this._viewChanged}
|
||||
.value=${idx.toString()}
|
||||
<ha-list-item-option
|
||||
.selected=${this._selectedViewIdx === idx}
|
||||
.disabled=${isStrategy && !this._params?.includeStrategyViews}
|
||||
?autofocus=${idx === 0 && !this._params!.allowDashboardChange}
|
||||
>
|
||||
<span>
|
||||
${view.title}${
|
||||
${
|
||||
view.icon
|
||||
? html`<ha-icon slot="start" .icon=${view.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiViewDashboard}
|
||||
></ha-svg-icon>`
|
||||
}
|
||||
<span slot="headline">
|
||||
${
|
||||
view.title ||
|
||||
this.hass.localize("ui.panel.lovelace.views.unnamed_view")
|
||||
}${
|
||||
isStrategy
|
||||
? ` (${this.hass.localize("ui.panel.lovelace.editor.select_view.strategy_type")})`
|
||||
: nothing
|
||||
}
|
||||
</span>
|
||||
|
||||
<ha-icon .icon=${view.icon} slot="graphic"></ha-icon>
|
||||
</ha-radio-list-item>
|
||||
</ha-list-item-option>
|
||||
`;
|
||||
})}
|
||||
</ha-list>
|
||||
</ha-list-selectable>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -223,12 +246,8 @@ export class HuiDialogSelectView extends LitElement {
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
private _viewChanged(e) {
|
||||
const view = Number(e.target.value);
|
||||
|
||||
if (!isNaN(view)) {
|
||||
this._selectedViewIdx = view;
|
||||
}
|
||||
private _viewChanged(ev: CustomEvent<number>) {
|
||||
this._selectedViewIdx = ev.detail;
|
||||
}
|
||||
|
||||
private _selectView(): void {
|
||||
@@ -251,14 +270,17 @@ export class HuiDialogSelectView extends LitElement {
|
||||
css`
|
||||
ha-select {
|
||||
width: 100%;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
h3,
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
mwc-radio-list-item {
|
||||
direction: ltr;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ export class LovelacePanel extends LitElement {
|
||||
// Refreshing a YAML config can trigger an update event. We will ignore
|
||||
// all update events while fetching the config and for 2 seconds after the config is back.
|
||||
// We ignore because we already have the latest config.
|
||||
if (this.lovelace && this.lovelace.mode === "yaml") {
|
||||
if (confMode === "yaml") {
|
||||
this._ignoreNextUpdateEvent = true;
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ export class LovelacePanel extends LitElement {
|
||||
} finally {
|
||||
this._loading = false;
|
||||
// Ignore updates for another 2 seconds.
|
||||
if (this.lovelace && this.lovelace.mode === "yaml") {
|
||||
if (confMode === "yaml") {
|
||||
setTimeout(() => {
|
||||
this._ignoreNextUpdateEvent = false;
|
||||
}, 2000);
|
||||
|
||||
@@ -92,16 +92,22 @@ export const filterUnavailableBatteryEntities = (
|
||||
return states[entityId]?.state === "unavailable";
|
||||
});
|
||||
|
||||
const computeBatteryTileCard = (
|
||||
const computeMaintenanceTileCard = (
|
||||
states: HomeAssistant["states"],
|
||||
entities: HomeAssistant["entities"],
|
||||
entityId: string
|
||||
): TileCardConfig => {
|
||||
const entity = entities[entityId];
|
||||
const deviceId = entity?.device_id;
|
||||
const deviceId = entities[entityId]?.device_id;
|
||||
if (!deviceId) {
|
||||
return { type: "tile", entity: entityId, name: { type: "entity" } };
|
||||
}
|
||||
const isProblem = states[entityId]?.attributes?.device_class === "problem";
|
||||
return {
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
name: { type: deviceId ? "device" : "entity" },
|
||||
name: isProblem
|
||||
? [{ type: "device" }, { type: "entity" }]
|
||||
: { type: "device" },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -130,7 +136,9 @@ const processAreasForBattery = (
|
||||
const areaCards: LovelaceCardConfig[] = [];
|
||||
|
||||
for (const entityId of areaBatteryEntities) {
|
||||
areaCards.push(computeBatteryTileCard(hass.entities, entityId));
|
||||
areaCards.push(
|
||||
computeMaintenanceTileCard(hass.states, hass.entities, entityId)
|
||||
);
|
||||
}
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
@@ -170,7 +178,9 @@ const processUnassignedEntities = (
|
||||
const cards: LovelaceCardConfig[] = [];
|
||||
|
||||
for (const entityId of unassignedEntities) {
|
||||
cards.push(computeBatteryTileCard(hass.entities, entityId));
|
||||
cards.push(
|
||||
computeMaintenanceTileCard(hass.states, hass.entities, entityId)
|
||||
);
|
||||
}
|
||||
|
||||
return cards;
|
||||
|
||||
@@ -12,17 +12,17 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { until } from "lit/directives/until";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { MediaProgressController } from "../../common/controllers/media-progress-controller";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityPickerDisplay } from "../../common/entity/compute_entity_name_display";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { MediaProgressController } from "../../common/controllers/media-progress-controller";
|
||||
import { VolumeSliderController } from "../../common/util/volume-slider";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-domain-icon";
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
computeMediaDescription,
|
||||
formatMediaTime,
|
||||
handleMediaControlClick,
|
||||
removeSecondaryStopControl,
|
||||
setMediaPlayerVolume,
|
||||
} from "../../data/media-player";
|
||||
import type { ResolvedMediaSource } from "../../data/media_source";
|
||||
@@ -170,7 +171,10 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
const controls: ControlButton[] | undefined = !this.narrow
|
||||
? computeMediaControls(stateObj, true)
|
||||
? removeSecondaryStopControl(
|
||||
stateObj,
|
||||
computeMediaControls(stateObj, true)
|
||||
)
|
||||
: (stateObj.state === "playing" &&
|
||||
(supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE) ||
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.STOP))) ||
|
||||
|
||||
@@ -31,7 +31,6 @@ import "../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-list";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-state-icon";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-two-pane-top-app-bar-fixed";
|
||||
@@ -240,14 +239,14 @@ class PanelTodo extends LitElement {
|
||||
<ha-list slot="pane" activatable>${listItems}</ha-list>
|
||||
${
|
||||
showPane && this.hass.user?.is_admin
|
||||
? html`<ha-list-item
|
||||
graphic="icon"
|
||||
? html`<ha-button
|
||||
slot="pane-footer"
|
||||
appearance="filled"
|
||||
@click=${this._addList}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="graphic"></ha-svg-icon>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.todo.create_list")}
|
||||
</ha-list-item>`
|
||||
</ha-button>`
|
||||
: nothing
|
||||
}
|
||||
<ha-dropdown
|
||||
@@ -462,6 +461,12 @@ class PanelTodo extends LitElement {
|
||||
--ha-button-box-shadow: var(--ha-box-shadow-l);
|
||||
}
|
||||
|
||||
ha-button[slot="pane-footer"] {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin: var(--ha-space-2) auto var(--ha-space-2);
|
||||
}
|
||||
|
||||
ha-dropdown.lists ha-dropdown-item {
|
||||
max-width: 80vw;
|
||||
}
|
||||
|
||||
@@ -857,7 +857,6 @@
|
||||
},
|
||||
"mode_composed": "Composed",
|
||||
"mode_custom": "Custom",
|
||||
"add": "Add",
|
||||
"search": "Search name",
|
||||
"custom_name": "Custom name",
|
||||
"no_more_parts": "Nothing left to add",
|
||||
@@ -944,7 +943,6 @@
|
||||
"collapse": "Collapse"
|
||||
},
|
||||
"entity-type-picker": {
|
||||
"add": "Add type",
|
||||
"no_types": "No types available"
|
||||
},
|
||||
"subpage-data-table": {
|
||||
@@ -6720,6 +6718,7 @@
|
||||
"script": "scripts"
|
||||
},
|
||||
"error": "{path} could not be loaded",
|
||||
"used_count": "{count, plural,\n =0 {Not in use}\n one {Used once}\n other {Used {count} times}\n}",
|
||||
"blueprint_in_use_title": "This blueprint is in use and cannot be deleted",
|
||||
"blueprint_in_use_text": "Please remove all below {type} that use this blueprint before deleting it. {list}",
|
||||
"blueprint_in_use_view_automation": "View automations",
|
||||
@@ -10536,6 +10535,7 @@
|
||||
"header": "Choose a view",
|
||||
"dashboard_label": "Dashboard",
|
||||
"views_label": "View",
|
||||
"views_heading": "Views of {dashboard}",
|
||||
"no_config": "No config found.",
|
||||
"no_views": "No views in this dashboard.",
|
||||
"strategy_type": "strategy"
|
||||
@@ -11386,6 +11386,14 @@
|
||||
"date": {
|
||||
"label": "Date",
|
||||
"description": "Select and order the date parts. Add a separator to control punctuation.",
|
||||
"presets": {
|
||||
"short": "Short",
|
||||
"medium": "Medium",
|
||||
"long": "Long",
|
||||
"full": "Full",
|
||||
"iso": "ISO 8601",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"sections": {
|
||||
"weekday": "Weekday",
|
||||
"day": "Day",
|
||||
@@ -11407,6 +11415,7 @@
|
||||
"separator-dash": "Dash",
|
||||
"separator-slash": "Slash",
|
||||
"separator-dot": "Dot",
|
||||
"separator-comma": "Comma",
|
||||
"separator-new-line": "New line"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { arrayWith, arrayWithout } from "../../../src/common/array/array-with";
|
||||
|
||||
describe("arrayWith", () => {
|
||||
it("works", () => {
|
||||
expect(arrayWith(undefined, "a")).toEqual(["a"]);
|
||||
expect(arrayWith(["a", "b"], "c")).toEqual(["a", "b", "c"]);
|
||||
expect(arrayWith(["a", "b"], "a")).toEqual(["a", "b"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrayWithout", () => {
|
||||
it("works", () => {
|
||||
expect(arrayWithout(["a", "b"], "a")).toEqual(["b"]);
|
||||
expect(arrayWithout(["a", "b"], "c")).toEqual(["a", "b"]);
|
||||
expect(arrayWithout(undefined, "a")).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import {
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import "../../src/components/ha-areas-floors-display-editor";
|
||||
import type {
|
||||
AreasFloorsDisplayValue,
|
||||
HaAreasFloorsDisplayEditor,
|
||||
} from "../../src/components/ha-areas-floors-display-editor";
|
||||
import type { HaItemDisplayEditor } from "../../src/components/ha-items-display-editor";
|
||||
import {
|
||||
areasContext,
|
||||
floorsContext,
|
||||
internationalizationContext,
|
||||
} from "../../src/data/context";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../src/types";
|
||||
|
||||
beforeAll(() => {
|
||||
// jsdom's ElementInternals lacks the validity API used by the webawesome
|
||||
// buttons that render inside the items editor.
|
||||
const internalsProto = window.ElementInternals.prototype as any;
|
||||
internalsProto.setValidity = vi.fn();
|
||||
internalsProto.setFormValue = vi.fn();
|
||||
Object.defineProperty(internalsProto, "validity", {
|
||||
get: () => ({ valid: true }),
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
class MockResizeObserver {
|
||||
observe = vi.fn();
|
||||
|
||||
unobserve = vi.fn();
|
||||
|
||||
disconnect = vi.fn();
|
||||
}
|
||||
|
||||
const UNASSIGNED_FLOOR = "__unassigned__";
|
||||
|
||||
const FLOORS = {
|
||||
floor_a: { floor_id: "floor_a", name: "A", level: 0 },
|
||||
floor_b: { floor_id: "floor_b", name: "B", level: 1 },
|
||||
} as unknown as HomeAssistant["floors"];
|
||||
|
||||
const area = (areaId: string, floorId: string | null) => ({
|
||||
area_id: areaId,
|
||||
name: areaId,
|
||||
floor_id: floorId,
|
||||
icon: null,
|
||||
});
|
||||
|
||||
const AREAS = Object.fromEntries(
|
||||
[
|
||||
area("a1", "floor_a"),
|
||||
area("a2", "floor_a"),
|
||||
area("a3", "floor_a"),
|
||||
area("b1", "floor_b"),
|
||||
area("b2", "floor_b"),
|
||||
area("b3", "floor_b"),
|
||||
area("u1", null),
|
||||
area("u2", null),
|
||||
].map((entry) => [entry.area_id, entry])
|
||||
) as unknown as HomeAssistant["areas"];
|
||||
|
||||
describe("ha-areas-floors-display-editor", () => {
|
||||
let host: HTMLDivElement | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("ResizeObserver", MockResizeObserver);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
host?.remove();
|
||||
host = undefined;
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const mount = async (
|
||||
value: AreasFloorsDisplayValue
|
||||
): Promise<HaAreasFloorsDisplayEditor> => {
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
new ContextProvider(host, {
|
||||
context: internationalizationContext,
|
||||
initialValue: {
|
||||
localize: (key: string) => key,
|
||||
} as unknown as HomeAssistantInternationalization,
|
||||
});
|
||||
new ContextProvider(host, { context: areasContext, initialValue: AREAS });
|
||||
new ContextProvider(host, { context: floorsContext, initialValue: FLOORS });
|
||||
const el = document.createElement(
|
||||
"ha-areas-floors-display-editor"
|
||||
) as HaAreasFloorsDisplayEditor;
|
||||
el.value = value;
|
||||
host.appendChild(el);
|
||||
await el.updateComplete;
|
||||
return el;
|
||||
};
|
||||
|
||||
// Drags the first visible area of a floor below the second one and returns
|
||||
// the configuration the editor emits.
|
||||
const reorderFirstTwo = async (
|
||||
el: HaAreasFloorsDisplayEditor,
|
||||
floorId: string
|
||||
): Promise<AreasFloorsDisplayValue> => {
|
||||
const editor = Array.from(
|
||||
el.shadowRoot!.querySelectorAll("ha-items-display-editor")
|
||||
).find(
|
||||
(child) =>
|
||||
(child as HaItemDisplayEditor & { floorId: string }).floorId === floorId
|
||||
)!;
|
||||
await editor.updateComplete;
|
||||
const emitted: AreasFloorsDisplayValue[] = [];
|
||||
el.addEventListener(
|
||||
"value-changed",
|
||||
(ev) => emitted.push((ev as CustomEvent).detail.value),
|
||||
{ once: true }
|
||||
);
|
||||
editor.shadowRoot!.querySelector("ha-sortable")!.dispatchEvent(
|
||||
new CustomEvent("item-moved", {
|
||||
detail: { oldIndex: 0, newIndex: 1 },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
expect(emitted).toHaveLength(1);
|
||||
return emitted[0];
|
||||
};
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "hidden area on another floor",
|
||||
floorId: "floor_a",
|
||||
value: {
|
||||
areas_display: { hidden: ["b1"], order: ["b3", "b2"] },
|
||||
},
|
||||
expected: {
|
||||
hidden: ["b1"],
|
||||
order: ["a2", "a1", "a3", "b3", "b2"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hidden areas on the reordered and another floor",
|
||||
floorId: "floor_a",
|
||||
value: { areas_display: { hidden: ["a3", "b1"] } },
|
||||
expected: { hidden: ["a3", "b1"], order: ["a2", "a1"] },
|
||||
},
|
||||
{
|
||||
name: "reordering areas without a floor",
|
||||
floorId: UNASSIGNED_FLOOR,
|
||||
value: { areas_display: { hidden: ["b1"] } },
|
||||
expected: { hidden: ["b1"], order: ["u2", "u1"] },
|
||||
},
|
||||
{
|
||||
name: "no hidden areas",
|
||||
floorId: "floor_b",
|
||||
value: {},
|
||||
expected: { order: ["b2", "b1", "b3"] },
|
||||
},
|
||||
])(
|
||||
"keeps hidden areas unchanged when reordering: $name",
|
||||
async ({ floorId, value, expected }) => {
|
||||
const el = await mount(value);
|
||||
|
||||
const first = await reorderFirstTwo(el, floorId);
|
||||
expect(first.areas_display).toEqual(expected);
|
||||
|
||||
// Saving and reordering again must not grow the hidden list
|
||||
el.value = first;
|
||||
await el.updateComplete;
|
||||
const second = await reorderFirstTwo(el, floorId);
|
||||
expect(second.areas_display?.hidden).toEqual(expected.hidden);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import "../../../src/components/item/ha-list-item-base";
|
||||
import type { HaListItemBase } from "../../../src/components/item/ha-list-item-base";
|
||||
import "../../../src/components/list/ha-list-base";
|
||||
import type { HaListBase } from "../../../src/components/list/ha-list-base";
|
||||
|
||||
const renderList = async (count: number) => {
|
||||
const list = document.createElement("ha-list-base");
|
||||
const items = Array.from({ length: count }, () => {
|
||||
const item = document.createElement("ha-list-item-base");
|
||||
item.interactive = true;
|
||||
list.appendChild(item);
|
||||
return item;
|
||||
});
|
||||
document.body.appendChild(list);
|
||||
await list.updateComplete;
|
||||
await Promise.all(items.map((item) => item.updateComplete));
|
||||
return { list, items };
|
||||
};
|
||||
|
||||
const setDisabled = async (items: HaListItemBase[], disabled: boolean) => {
|
||||
items.forEach((item) => {
|
||||
item.disabled = disabled;
|
||||
});
|
||||
await Promise.all(items.map((item) => item.updateComplete));
|
||||
// Let the batched list update run
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
describe("ha-list-base", () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
it("follows rows that change their focusability", async () => {
|
||||
const { list, items } = await renderList(3);
|
||||
expect(items.map((item) => item.tabIndex)).toEqual([0, -1, -1]);
|
||||
|
||||
const update = vi.spyOn(list as HaListBase, "updateListItems");
|
||||
|
||||
// Rows removed while all are disabled, like during a delete
|
||||
await setDisabled(items, true);
|
||||
items[0].remove();
|
||||
await Promise.resolve();
|
||||
expect(items.map((item) => item.hasAttribute("tabindex"))).toEqual([
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
]);
|
||||
|
||||
update.mockClear();
|
||||
await setDisabled(items.slice(1), false);
|
||||
expect(update).toHaveBeenCalledTimes(1);
|
||||
expect(items.slice(1).map((item) => item.tabIndex)).toEqual([0, -1]);
|
||||
});
|
||||
|
||||
it("moves the Tab stop off an active row that gets disabled", async () => {
|
||||
const { items } = await renderList(2);
|
||||
|
||||
await setDisabled([items[0]], true);
|
||||
expect(items[0].hasAttribute("tabindex")).toBe(false);
|
||||
expect(items[1].tabIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { MediaPlayerEntity } from "../../src/data/media-player";
|
||||
import {
|
||||
computeMediaControls,
|
||||
MediaPlayerEntityFeature,
|
||||
removeSecondaryStopControl,
|
||||
} from "../../src/data/media-player";
|
||||
|
||||
const player = (
|
||||
state: string,
|
||||
supported_features: number,
|
||||
attributes: Partial<MediaPlayerEntity["attributes"]> = {}
|
||||
): MediaPlayerEntity =>
|
||||
({
|
||||
entity_id: "media_player.test",
|
||||
state,
|
||||
attributes: { supported_features, ...attributes },
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", parent_id: null, user_id: null },
|
||||
}) as MediaPlayerEntity;
|
||||
|
||||
const features = (...flags: number[]): number =>
|
||||
// eslint-disable-next-line no-bitwise
|
||||
flags.reduce((acc, flag) => acc | flag, 0);
|
||||
|
||||
const PLAY_PAUSE_STOP = features(
|
||||
MediaPlayerEntityFeature.PLAY,
|
||||
MediaPlayerEntityFeature.PAUSE,
|
||||
MediaPlayerEntityFeature.STOP
|
||||
);
|
||||
|
||||
const actions = (stateObj: MediaPlayerEntity, extended: boolean) =>
|
||||
computeMediaControls(stateObj, extended)?.map((c) => c.action);
|
||||
|
||||
describe("computeMediaControls stop button", () => {
|
||||
it.each([
|
||||
{ state: "playing", extended: true, main: "media_pause" },
|
||||
{ state: "playing", extended: false, main: "media_pause" },
|
||||
{ state: "paused", extended: true, main: "media_play" },
|
||||
{ state: "paused", extended: false, main: "media_play" },
|
||||
])(
|
||||
"adds stop next to play/pause when $state (extended: $extended)",
|
||||
({ state, extended, main }) => {
|
||||
expect(actions(player(state, PLAY_PAUSE_STOP), extended)).toEqual([
|
||||
main,
|
||||
"media_stop",
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
it("does not add stop when already stopped", () => {
|
||||
expect(actions(player("idle", PLAY_PAUSE_STOP), true)).toEqual([
|
||||
"media_play",
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses stop as the main button without duplicating it when pause is unsupported", () => {
|
||||
expect(
|
||||
actions(
|
||||
player(
|
||||
"playing",
|
||||
features(MediaPlayerEntityFeature.PLAY, MediaPlayerEntityFeature.STOP)
|
||||
),
|
||||
true
|
||||
)
|
||||
).toEqual(["media_stop"]);
|
||||
});
|
||||
|
||||
it("does not duplicate stop for assumed state players", () => {
|
||||
expect(
|
||||
actions(player("playing", PLAY_PAUSE_STOP, { assumed_state: true }), true)
|
||||
).toEqual(["media_play", "media_pause", "media_stop"]);
|
||||
});
|
||||
});
|
||||
|
||||
// The callers always pass the output of computeMediaControls, so test the
|
||||
// two together instead of hand-written control lists.
|
||||
const mainActions = (stateObj: MediaPlayerEntity) =>
|
||||
removeSecondaryStopControl(
|
||||
stateObj,
|
||||
computeMediaControls(stateObj, true)
|
||||
)?.map((c) => c.action);
|
||||
|
||||
describe("removeSecondaryStopControl", () => {
|
||||
it("removes only stop when play or pause is shown", () => {
|
||||
const supported = features(
|
||||
PLAY_PAUSE_STOP,
|
||||
MediaPlayerEntityFeature.TURN_OFF,
|
||||
MediaPlayerEntityFeature.PREVIOUS_TRACK,
|
||||
MediaPlayerEntityFeature.NEXT_TRACK
|
||||
);
|
||||
expect(mainActions(player("playing", supported))).toEqual([
|
||||
"turn_off",
|
||||
"media_previous_track",
|
||||
"media_pause",
|
||||
"media_next_track",
|
||||
]);
|
||||
expect(mainActions(player("paused", supported))).toEqual([
|
||||
"turn_off",
|
||||
"media_previous_track",
|
||||
"media_play",
|
||||
"media_next_track",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps stop when it is the only way to stop playback", () => {
|
||||
expect(
|
||||
mainActions(
|
||||
player(
|
||||
"playing",
|
||||
features(MediaPlayerEntityFeature.PLAY, MediaPlayerEntityFeature.STOP)
|
||||
)
|
||||
)
|
||||
).toEqual(["media_stop"]);
|
||||
});
|
||||
|
||||
it("keeps play, pause and stop together for assumed state players", () => {
|
||||
expect(
|
||||
mainActions(player("playing", PLAY_PAUSE_STOP, { assumed_state: true }))
|
||||
).toEqual(["media_play", "media_pause", "media_stop"]);
|
||||
});
|
||||
|
||||
it("passes through missing controls of unavailable players", () => {
|
||||
expect(mainActions(player("unavailable", PLAY_PAUSE_STOP))).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,13 @@
|
||||
import { assert, describe, it } from "vitest";
|
||||
import type { ClockCardDatePart } from "../../../../src/panels/lovelace/cards/types";
|
||||
import type {
|
||||
ClockCardDatePart,
|
||||
ClockCardDatePreset,
|
||||
} from "../../../../src/panels/lovelace/cards/types";
|
||||
import {
|
||||
formatClockCardDate,
|
||||
formatClockCardDatePreset,
|
||||
getClockCardDateConfig,
|
||||
getClockCardDatePresetParts,
|
||||
getClockCardDateTimeFormatOptions,
|
||||
hasClockCardDate,
|
||||
} from "../../../../src/panels/lovelace/cards/clock/clock-date-format";
|
||||
@@ -258,4 +263,144 @@ describe("clock-date-format", () => {
|
||||
|
||||
assert.equal(result, "11 2024");
|
||||
});
|
||||
|
||||
it("reads a preset date format", () => {
|
||||
assert.deepEqual(getClockCardDateConfig({ date_format: "long" }), {
|
||||
parts: [],
|
||||
preset: "long",
|
||||
});
|
||||
assert.equal(hasClockCardDate({ date_format: "short" }), true);
|
||||
assert.deepEqual(
|
||||
getClockCardDateTimeFormatOptions({ parts: [], preset: "full" }),
|
||||
{ dateStyle: "full" }
|
||||
);
|
||||
});
|
||||
|
||||
it("formats presets with the locale's date styles", () => {
|
||||
const format = (preset: ClockCardDatePreset, language = "en-GB") =>
|
||||
formatClockCardDate(date, { parts: [], preset }, language, "UTC");
|
||||
|
||||
assert.equal(format("short"), "08/11/2024");
|
||||
assert.equal(format("medium"), "8 Nov 2024");
|
||||
assert.equal(format("long"), "8 November 2024");
|
||||
assert.equal(format("short", "en-US"), "11/8/24");
|
||||
});
|
||||
|
||||
it("formats the ISO preset the same in every language", () => {
|
||||
const format = (language: string, timeZone: string) =>
|
||||
formatClockCardDatePreset(date, "iso", language, timeZone);
|
||||
|
||||
assert.equal(format("en-US", "UTC"), "2024-11-08");
|
||||
assert.equal(format("ar-EG", "UTC"), "2024-11-08");
|
||||
assert.equal(format("en", "Pacific/Kiritimati"), "2024-11-09");
|
||||
});
|
||||
|
||||
it("converts presets into matching custom parts", () => {
|
||||
assert.deepEqual(getClockCardDatePresetParts("short", "en-GB"), [
|
||||
"day-2-digit",
|
||||
"separator-slash",
|
||||
"month-2-digit",
|
||||
"separator-slash",
|
||||
"year-numeric",
|
||||
]);
|
||||
assert.deepEqual(getClockCardDatePresetParts("full", "en-US"), [
|
||||
"weekday-long",
|
||||
"separator-comma",
|
||||
"month-long",
|
||||
"day-numeric",
|
||||
"separator-comma",
|
||||
"year-numeric",
|
||||
]);
|
||||
assert.deepEqual(getClockCardDatePresetParts("short", "de"), [
|
||||
"day-2-digit",
|
||||
"separator-dot",
|
||||
"month-2-digit",
|
||||
"separator-dot",
|
||||
"year-2-digit",
|
||||
]);
|
||||
|
||||
(["en-GB", "en-US"] as const).forEach((language) => {
|
||||
(["short", "medium", "long", "full", "iso"] as const).forEach(
|
||||
(preset) => {
|
||||
const parts = getClockCardDatePresetParts(preset, language);
|
||||
assert.equal(
|
||||
formatClockCardDate(date, { parts }, language, "UTC"),
|
||||
formatClockCardDate(date, { parts: [], preset }, language, "UTC")
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the long month when a language inflects it next to the day", () => {
|
||||
(["pl", "ru"] as const).forEach((language) => {
|
||||
assert.deepEqual(
|
||||
getClockCardDatePresetParts("long", language).slice(0, 2),
|
||||
["day-numeric", "month-long"]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("drops a separator that a language already adds to the field", () => {
|
||||
assert.deepEqual(getClockCardDatePresetParts("short", "ja"), [
|
||||
"year-numeric",
|
||||
"month-2-digit",
|
||||
"day-2-digit",
|
||||
]);
|
||||
assert.deepEqual(getClockCardDatePresetParts("short", "ko"), [
|
||||
"year-2-digit",
|
||||
"month-numeric",
|
||||
"day-numeric",
|
||||
]);
|
||||
assert.deepEqual(getClockCardDatePresetParts("short", "cs"), [
|
||||
"day-2-digit",
|
||||
"month-2-digit",
|
||||
"separator-dot",
|
||||
"year-2-digit",
|
||||
]);
|
||||
assert.deepEqual(getClockCardDatePresetParts("short", "nb"), [
|
||||
"day-2-digit",
|
||||
"month-2-digit",
|
||||
"year-numeric",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps numeric months numeric and separators around direction marks", () => {
|
||||
assert.deepEqual(getClockCardDatePresetParts("short", "es"), [
|
||||
"day-numeric",
|
||||
"separator-slash",
|
||||
"month-numeric",
|
||||
"separator-slash",
|
||||
"year-2-digit",
|
||||
]);
|
||||
assert.include(getClockCardDatePresetParts("short", "ga"), "month-numeric");
|
||||
assert.deepEqual(getClockCardDatePresetParts("short", "ar"), [
|
||||
"day-numeric",
|
||||
"separator-slash",
|
||||
"month-numeric",
|
||||
"separator-slash",
|
||||
"year-numeric",
|
||||
]);
|
||||
});
|
||||
|
||||
it("only matches separators exactly", () => {
|
||||
assert.deepEqual(getClockCardDatePresetParts("long", "de"), [
|
||||
"day-numeric",
|
||||
"month-long",
|
||||
"year-numeric",
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders a comma separator with a following space", () => {
|
||||
const result = formatClockCardDate(
|
||||
date,
|
||||
{
|
||||
parts: ["weekday-long", "separator-comma", "month-long", "day-numeric"],
|
||||
},
|
||||
"en",
|
||||
"UTC"
|
||||
);
|
||||
|
||||
assert.equal(result, "Friday, November 8");
|
||||
});
|
||||
});
|
||||
@@ -2,8 +2,13 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
filterLowBatteryEntities,
|
||||
filterProblemEntities,
|
||||
MaintenanceViewStrategy,
|
||||
} from "../../../../src/panels/maintenance/strategies/maintenance-view-strategy";
|
||||
import { mockEntity } from "../../../common/entity/context/context-mock";
|
||||
import {
|
||||
mockDevice,
|
||||
mockEntity,
|
||||
} from "../../../common/entity/context/context-mock";
|
||||
import type { LovelaceSectionConfig } from "../../../../src/data/lovelace/config/section";
|
||||
import { createMockEntityState, createMockHass } from "../../../fixtures/hass";
|
||||
|
||||
describe("filterLowBatteryEntities", () => {
|
||||
@@ -209,3 +214,70 @@ describe("filterProblemEntities", () => {
|
||||
).toEqual(["binary_sensor.has_problem"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MaintenanceViewStrategy.generate", () => {
|
||||
it("names problem tiles after the device and the entity", async () => {
|
||||
const hass = createMockHass(
|
||||
{
|
||||
"sensor.microwave_battery": createMockEntityState(
|
||||
"sensor.microwave_battery",
|
||||
"80",
|
||||
{ device_class: "battery" }
|
||||
),
|
||||
"binary_sensor.microwave_overheating": createMockEntityState(
|
||||
"binary_sensor.microwave_overheating",
|
||||
"off",
|
||||
{ device_class: "problem" }
|
||||
),
|
||||
"binary_sensor.standalone_problem": createMockEntityState(
|
||||
"binary_sensor.standalone_problem",
|
||||
"off",
|
||||
{ device_class: "problem" }
|
||||
),
|
||||
},
|
||||
{
|
||||
devices: {
|
||||
microwave: mockDevice({ id: "microwave", name: "Microwave" }),
|
||||
},
|
||||
entities: {
|
||||
"sensor.microwave_battery": mockEntity({
|
||||
entity_id: "sensor.microwave_battery",
|
||||
device_id: "microwave",
|
||||
}),
|
||||
"binary_sensor.microwave_overheating": mockEntity({
|
||||
entity_id: "binary_sensor.microwave_overheating",
|
||||
device_id: "microwave",
|
||||
}),
|
||||
"binary_sensor.standalone_problem": mockEntity({
|
||||
entity_id: "binary_sensor.standalone_problem",
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const view = await MaintenanceViewStrategy.generate(
|
||||
{ type: "maintenance" },
|
||||
hass
|
||||
);
|
||||
|
||||
const section = view.sections?.[0] as LovelaceSectionConfig;
|
||||
|
||||
expect(section.cards?.slice(1)).toEqual([
|
||||
{
|
||||
type: "tile",
|
||||
entity: "sensor.microwave_battery",
|
||||
name: { type: "device" },
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "binary_sensor.microwave_overheating",
|
||||
name: [{ type: "device" }, { type: "entity" }],
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "binary_sensor.standalone_problem",
|
||||
name: { type: "entity" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user